From 9ce1e3d587c77e658a4161574603e349f160883f Mon Sep 17 00:00:00 2001 From: xclhove <2959169303@qq.com> Date: Fri, 6 Sep 2024 17:54:36 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增笔记分类模块,可以对笔记进行分类,便于管理。 新增笔记分享记录模块,可以分享私有笔记以及管理分享记录。 新增用户图片模块用于记录用户上传的图片,修改图片模块,确保相同的图片只存储一份以节省空间。 修改笔记模块,删除关键词属性,新增分类ID属性。 修改用户模块,允许用户指定主页显示一篇笔记,增加用户存储空间限制以防止恶意上传。 统一 redis 的 key 命名。 新增图片验证码功能,防止恶意请求。 集成 elasticsearch,用于提高搜索笔记的速度和命中率,并对搜索内容进行高亮显示。 明确请求所需参数并去除无效的属性,使用SpringBoot-validation对参数进行校验。 --- README-EN.md | 26 - README.md | 26 - docs/api-doc.md | 2577 +++++++++++++++++ docs/images/database.png | Bin 0 -> 269139 bytes pom.xml | 97 +- readme-en.md | 77 + readme.md | 74 +- ...XnoteApplication.java => Application.java} | 6 +- .../Interceptor/AdminJwtInterceptor.java | 60 - .../xnote/Interceptor/DeviceInterceptor.java | 28 - .../xnote/Interceptor/IPInterceptor.java | 82 - .../xnote/Interceptor/UserJwtInterceptor.java | 89 - .../validator/UserJwtValidator.java | 83 - .../xnote/config/GlobalExceptionHandler.java | 115 +- .../xnote/config/InterceptorConfig.java | 14 +- .../{MailConfig.java => JavaMailConfig.java} | 5 +- .../com/xclhove/xnote/config/MinioConfig.java | 19 +- .../xnote/config/MybatisPlusConfig.java | 8 + .../com/xclhove/xnote/config/RedisConfig.java | 46 - .../xclhove/xnote/config/RunnerConfig.java | 31 - .../xclhove/xnote/config/SwaggerConfig.java | 42 - .../com/xclhove/xnote/config/XnoteConfig.java | 117 + .../xclhove/xnote/constant/EsConstant.java | 14 + .../com/xclhove/xnote/constant/RedisKey.java | 76 +- .../xnote/constant/RequestHeaderKey.java | 2 +- .../xclhove/xnote/constant/TreadLocalKey.java | 2 +- .../xnote/controller/ImageController.java | 136 +- .../xnote/controller/NoteController.java | 167 +- .../xnote/controller/NoteTypeController.java | 70 + .../controller/RequestErrorController.java | 15 +- .../controller/ShareNoteRecordController.java | 79 + .../xnote/controller/SwaggerController.java | 41 - .../xnote/controller/UserController.java | 209 +- .../VerificationCodeController.java | 118 + .../xnote/entity/attribute/NoteKeyword.java | 18 - .../xnote/entity/dto/ImagePageDTO.java | 23 - .../xclhove/xnote/entity/dto/NotePageDTO.java | 35 - .../xclhove/xnote/entity/dto/ObjectList.java | 17 - .../com/xclhove/xnote/entity/dto/PageDTO.java | 31 - .../com/xclhove/xnote/entity/dto/UserDTO.java | 44 - .../com/xclhove/xnote/entity/table/Image.java | 40 - .../com/xclhove/xnote/entity/table/Note.java | 57 - .../com/xclhove/xnote/entity/table/User.java | 42 - .../com/xclhove/xnote/enums/ResultType.java | 39 + .../xnote/enums/result/ResultType.java | 39 - .../exception/AbstractServiceException.java | 28 + .../exception/AdminServiceException.java | 22 - .../xnote/exception/AdminTokenException.java | 22 - .../exception/DeviceFrequencyException.java | 20 + .../exception/ImageServiceException.java | 9 +- .../xnote/exception/IpFrequencyException.java | 5 +- .../xnote/exception/NotFoundException.java | 11 +- .../exception/NoteAccessCodeException.java | 22 - .../xnote/exception/NoteServiceException.java | 9 +- .../exception/NoteTypeServiceException.java | 21 + .../OperationFrequencyException.java | 22 - .../exception/ParameterValidateException.java | 9 +- .../xnote/exception/ServiceException.java | 23 - .../ShareNoteRecordServiceException.java | 21 + .../xnote/exception/SystemException.java | 21 + .../exception/UserImageServiceException.java | 21 + .../xnote/exception/UserServiceException.java | 9 +- .../xnote/exception/UserTokenException.java | 7 +- .../exception/VerificationCodeException.java | 9 +- .../xnote/interceptor/DeviceInterceptor.java | 80 + .../xnote/interceptor/IpInterceptor.java | 107 + .../ServiceInterceptor.java | 2 +- .../interceptor/UserTokenInterceptor.java | 97 + .../com/xclhove/xnote/mapper/AdminMapper.java | 12 - .../com/xclhove/xnote/mapper/ImageMapper.java | 14 +- .../com/xclhove/xnote/mapper/NoteMapper.java | 14 +- .../xclhove/xnote/mapper/NoteTypeMapper.java | 11 + .../xnote/mapper/ShareNoteRecordMapper.java | 32 + .../xclhove/xnote/mapper/UserImageMapper.java | 10 + .../com/xclhove/xnote/mapper/UserMapper.java | 5 +- .../MybatisPlusPrintSqlInterceptor.java | 197 ++ .../com/xclhove/xnote/pojo/dto/UserDTO.java | 40 + .../enums}/NoteIsPublic.java | 2 +- .../enums}/UserStatus.java | 10 +- .../com/xclhove/xnote/pojo/es/NoteDoc.java | 83 + .../xnote/pojo/form/note/NoteAddForm.java | 35 + .../xnote/pojo/form/note/NoteUpdateForm.java | 33 + .../pojo/form/note/NoteUpdateTypeForm.java | 27 + .../pojo/form/noteType/NoteTypeAddForm.java | 26 + .../form/noteType/NoteTypeUpdateForm.java | 31 + .../xnote/pojo/form/rule/NoteFormRule.java | 12 + .../pojo/form/rule/NoteTypeFormRule.java | 11 + .../xnote/pojo/form/rule/UserFormRule.java | 23 + .../ShareNoteRecordCreateForm.java | 30 + .../ShareNoteRecordUpdateForm.java | 22 + .../xnote/pojo/form/user/UserLoginForm.java | 29 + .../pojo/form/user/UserRegisterForm.java | 37 + .../pojo/form/user/UserUpdateEmailForm.java | 28 + .../xnote/pojo/form/user/UserUpdateForm.java | 27 + .../form/user/UserUpdatePasswordForm.java | 24 + .../com/xclhove/xnote/pojo/table/Image.java | 28 + .../com/xclhove/xnote/pojo/table/Note.java | 38 + .../xclhove/xnote/pojo/table/NoteType.java | 25 + .../xnote/pojo/table/ShareNoteRecord.java | 32 + .../table/Admin.java => pojo/table/User.java} | 32 +- .../xclhove/xnote/pojo/table/UserImage.java | 26 + .../com/xclhove/xnote/pojo/vo/PageVO.java | 26 + .../xclhove/xnote/pojo/vo/SearchNoteVO.java | 29 + .../xnote/pojo/vo/SearchUserImageVO.java | 26 + .../xnote/pojo/vo/ShareNoteRecordVO.java | 27 + .../xnote/repository/NoteRepository.java | 10 + .../xclhove/xnote/runner/AbstractRunner.java | 44 - .../xnote/runner/ElasticSearchRunner.java | 59 + .../com/xclhove/xnote/runner/EmailRunner.java | 45 + .../com/xclhove/xnote/runner/ImageRunner.java | 73 + .../com/xclhove/xnote/runner/MinioRunner.java | 26 +- .../com/xclhove/xnote/runner/NoteRunner.java | 147 +- .../com/xclhove/xnote/runner/RedisRunner.java | 44 +- .../xclhove/xnote/service/AdminService.java | 52 - .../xclhove/xnote/service/ImageService.java | 293 +- .../xclhove/xnote/service/NoteService.java | 346 ++- .../xnote/service/NoteTypeService.java | 173 ++ .../xnote/service/ShareNoteRecordService.java | 143 + .../xnote/service/UserImageService.java | 139 + .../xclhove/xnote/service/UserService.java | 277 +- .../service/VerificationCodeService.java | 108 + .../xnote/service/impl/AdminServiceImpl.java | 55 - .../xnote/service/impl/ImageServiceImpl.java | 250 -- .../xnote/service/impl/NoteServiceImpl.java | 164 -- .../xnote/service/impl/UserServiceImpl.java | 196 -- .../xclhove/xnote/tool/ElasticsearchTool.java | 20 + .../com/xclhove/xnote/tool/EmailTool.java | 26 +- .../com/xclhove/xnote/tool/MinioTool.java | 197 +- .../com/xclhove/xnote/tool/RedisTool.java | 163 +- .../xclhove/xnote/{util => tool}/Result.java | 22 +- .../xclhove/xnote/tool/ThreadLocalTool.java | 44 + .../com/xclhove/xnote/tool/TokenTool.java | 39 - .../com/xclhove/xnote/tool/UserTokenTool.java | 39 + .../com/xclhove/xnote/util/ByteSizeUtil.java | 35 + .../xclhove/xnote/util/ElasticsearchUtil.java | 77 + .../com/xclhove/xnote/util/EmailUtil.java | 9 +- .../java/com/xclhove/xnote/util/Md5Util.java | 36 + .../com/xclhove/xnote/util/PackageUtil.java | 16 + .../com/xclhove/xnote/util/RequestUtil.java | 44 +- .../xclhove/xnote/util/SubclassFinder.java | 8 +- .../xclhove/xnote/util/ThreadLocalUtil.java | 17 +- .../com/xclhove/xnote/util/TokenUtil.java | 60 - .../java/com/xclhove/xnote/util/UuidUtil.java | 27 + .../xnote/util/ValidateCodeImageUtil.java | 111 + .../xclhove/xnote/util/VariableNameUtil.java | 33 + .../xnote/util/VerificationCodeUtil.java | 38 +- src/main/resources/application-dev.yml | 50 + src/main/resources/application-flyway.yml | 5 + src/main/resources/application-logging.yml | 22 + .../resources/application-mybatis-plus.yml | 9 + src/main/resources/application-release.yml | 50 + src/main/resources/application-spring.yml | 5 + src/main/resources/application.yml | 106 +- .../db/migration/V2.0__update_tables.sql | 91 + src/main/resources/mapper/ImageMapper.xml | 13 + src/main/resources/mapper/NoteMapper.xml | 18 + src/main/resources/mapper/NoteTypeMapper.xml | 5 + .../mapper/ShareNoteRecordMapper.xml | 5 + src/main/resources/mapper/UserImageMapper.xml | 5 + src/main/resources/mapper/UserMapper.xml | 5 + src/main/resources/static/index.html | 6 +- 161 files changed, 7840 insertions(+), 2907 deletions(-) delete mode 100644 README-EN.md delete mode 100644 README.md create mode 100644 docs/api-doc.md create mode 100644 docs/images/database.png create mode 100644 readme-en.md rename src/main/java/com/xclhove/xnote/{XnoteApplication.java => Application.java} (59%) delete mode 100644 src/main/java/com/xclhove/xnote/Interceptor/AdminJwtInterceptor.java delete mode 100644 src/main/java/com/xclhove/xnote/Interceptor/DeviceInterceptor.java delete mode 100644 src/main/java/com/xclhove/xnote/Interceptor/IPInterceptor.java delete mode 100644 src/main/java/com/xclhove/xnote/Interceptor/UserJwtInterceptor.java delete mode 100644 src/main/java/com/xclhove/xnote/Interceptor/validator/UserJwtValidator.java rename src/main/java/com/xclhove/xnote/config/{MailConfig.java => JavaMailConfig.java} (91%) delete mode 100644 src/main/java/com/xclhove/xnote/config/RedisConfig.java delete mode 100644 src/main/java/com/xclhove/xnote/config/RunnerConfig.java delete mode 100644 src/main/java/com/xclhove/xnote/config/SwaggerConfig.java create mode 100644 src/main/java/com/xclhove/xnote/config/XnoteConfig.java create mode 100644 src/main/java/com/xclhove/xnote/constant/EsConstant.java create mode 100644 src/main/java/com/xclhove/xnote/controller/NoteTypeController.java create mode 100644 src/main/java/com/xclhove/xnote/controller/ShareNoteRecordController.java delete mode 100644 src/main/java/com/xclhove/xnote/controller/SwaggerController.java create mode 100644 src/main/java/com/xclhove/xnote/controller/VerificationCodeController.java delete mode 100644 src/main/java/com/xclhove/xnote/entity/attribute/NoteKeyword.java delete mode 100644 src/main/java/com/xclhove/xnote/entity/dto/ImagePageDTO.java delete mode 100644 src/main/java/com/xclhove/xnote/entity/dto/NotePageDTO.java delete mode 100644 src/main/java/com/xclhove/xnote/entity/dto/ObjectList.java delete mode 100644 src/main/java/com/xclhove/xnote/entity/dto/PageDTO.java delete mode 100644 src/main/java/com/xclhove/xnote/entity/dto/UserDTO.java delete mode 100644 src/main/java/com/xclhove/xnote/entity/table/Image.java delete mode 100644 src/main/java/com/xclhove/xnote/entity/table/Note.java delete mode 100644 src/main/java/com/xclhove/xnote/entity/table/User.java create mode 100644 src/main/java/com/xclhove/xnote/enums/ResultType.java delete mode 100644 src/main/java/com/xclhove/xnote/enums/result/ResultType.java create mode 100644 src/main/java/com/xclhove/xnote/exception/AbstractServiceException.java delete mode 100644 src/main/java/com/xclhove/xnote/exception/AdminServiceException.java delete mode 100644 src/main/java/com/xclhove/xnote/exception/AdminTokenException.java create mode 100644 src/main/java/com/xclhove/xnote/exception/DeviceFrequencyException.java delete mode 100644 src/main/java/com/xclhove/xnote/exception/NoteAccessCodeException.java create mode 100644 src/main/java/com/xclhove/xnote/exception/NoteTypeServiceException.java delete mode 100644 src/main/java/com/xclhove/xnote/exception/OperationFrequencyException.java delete mode 100644 src/main/java/com/xclhove/xnote/exception/ServiceException.java create mode 100644 src/main/java/com/xclhove/xnote/exception/ShareNoteRecordServiceException.java create mode 100644 src/main/java/com/xclhove/xnote/exception/SystemException.java create mode 100644 src/main/java/com/xclhove/xnote/exception/UserImageServiceException.java create mode 100644 src/main/java/com/xclhove/xnote/interceptor/DeviceInterceptor.java create mode 100644 src/main/java/com/xclhove/xnote/interceptor/IpInterceptor.java rename src/main/java/com/xclhove/xnote/{Interceptor => interceptor}/ServiceInterceptor.java (93%) create mode 100644 src/main/java/com/xclhove/xnote/interceptor/UserTokenInterceptor.java delete mode 100644 src/main/java/com/xclhove/xnote/mapper/AdminMapper.java create mode 100644 src/main/java/com/xclhove/xnote/mapper/NoteTypeMapper.java create mode 100644 src/main/java/com/xclhove/xnote/mapper/ShareNoteRecordMapper.java create mode 100644 src/main/java/com/xclhove/xnote/mapper/UserImageMapper.java create mode 100644 src/main/java/com/xclhove/xnote/mybatisplus/MybatisPlusPrintSqlInterceptor.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/dto/UserDTO.java rename src/main/java/com/xclhove/xnote/{enums/entityattribute => pojo/enums}/NoteIsPublic.java (89%) rename src/main/java/com/xclhove/xnote/{enums/entityattribute => pojo/enums}/UserStatus.java (75%) create mode 100644 src/main/java/com/xclhove/xnote/pojo/es/NoteDoc.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/form/note/NoteAddForm.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/form/note/NoteUpdateForm.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/form/note/NoteUpdateTypeForm.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/form/noteType/NoteTypeAddForm.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/form/noteType/NoteTypeUpdateForm.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/form/rule/NoteFormRule.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/form/rule/NoteTypeFormRule.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/form/rule/UserFormRule.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/form/shareNoteRecord/ShareNoteRecordCreateForm.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/form/shareNoteRecord/ShareNoteRecordUpdateForm.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/form/user/UserLoginForm.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/form/user/UserRegisterForm.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/form/user/UserUpdateEmailForm.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/form/user/UserUpdateForm.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/form/user/UserUpdatePasswordForm.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/table/Image.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/table/Note.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/table/NoteType.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/table/ShareNoteRecord.java rename src/main/java/com/xclhove/xnote/{entity/table/Admin.java => pojo/table/User.java} (50%) create mode 100644 src/main/java/com/xclhove/xnote/pojo/table/UserImage.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/vo/PageVO.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/vo/SearchNoteVO.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/vo/SearchUserImageVO.java create mode 100644 src/main/java/com/xclhove/xnote/pojo/vo/ShareNoteRecordVO.java create mode 100644 src/main/java/com/xclhove/xnote/repository/NoteRepository.java delete mode 100644 src/main/java/com/xclhove/xnote/runner/AbstractRunner.java create mode 100644 src/main/java/com/xclhove/xnote/runner/ElasticSearchRunner.java create mode 100644 src/main/java/com/xclhove/xnote/runner/EmailRunner.java create mode 100644 src/main/java/com/xclhove/xnote/runner/ImageRunner.java delete mode 100644 src/main/java/com/xclhove/xnote/service/AdminService.java create mode 100644 src/main/java/com/xclhove/xnote/service/NoteTypeService.java create mode 100644 src/main/java/com/xclhove/xnote/service/ShareNoteRecordService.java create mode 100644 src/main/java/com/xclhove/xnote/service/UserImageService.java create mode 100644 src/main/java/com/xclhove/xnote/service/VerificationCodeService.java delete mode 100644 src/main/java/com/xclhove/xnote/service/impl/AdminServiceImpl.java delete mode 100644 src/main/java/com/xclhove/xnote/service/impl/ImageServiceImpl.java delete mode 100644 src/main/java/com/xclhove/xnote/service/impl/NoteServiceImpl.java delete mode 100644 src/main/java/com/xclhove/xnote/service/impl/UserServiceImpl.java create mode 100644 src/main/java/com/xclhove/xnote/tool/ElasticsearchTool.java rename src/main/java/com/xclhove/xnote/{util => tool}/Result.java (56%) create mode 100644 src/main/java/com/xclhove/xnote/tool/ThreadLocalTool.java delete mode 100644 src/main/java/com/xclhove/xnote/tool/TokenTool.java create mode 100644 src/main/java/com/xclhove/xnote/tool/UserTokenTool.java create mode 100644 src/main/java/com/xclhove/xnote/util/ByteSizeUtil.java create mode 100644 src/main/java/com/xclhove/xnote/util/ElasticsearchUtil.java create mode 100644 src/main/java/com/xclhove/xnote/util/Md5Util.java create mode 100644 src/main/java/com/xclhove/xnote/util/PackageUtil.java delete mode 100644 src/main/java/com/xclhove/xnote/util/TokenUtil.java create mode 100644 src/main/java/com/xclhove/xnote/util/UuidUtil.java create mode 100644 src/main/java/com/xclhove/xnote/util/ValidateCodeImageUtil.java create mode 100644 src/main/java/com/xclhove/xnote/util/VariableNameUtil.java create mode 100644 src/main/resources/application-dev.yml create mode 100644 src/main/resources/application-flyway.yml create mode 100644 src/main/resources/application-logging.yml create mode 100644 src/main/resources/application-mybatis-plus.yml create mode 100644 src/main/resources/application-release.yml create mode 100644 src/main/resources/application-spring.yml create mode 100644 src/main/resources/db/migration/V2.0__update_tables.sql create mode 100644 src/main/resources/mapper/ImageMapper.xml create mode 100644 src/main/resources/mapper/NoteMapper.xml create mode 100644 src/main/resources/mapper/NoteTypeMapper.xml create mode 100644 src/main/resources/mapper/ShareNoteRecordMapper.xml create mode 100644 src/main/resources/mapper/UserImageMapper.xml create mode 100644 src/main/resources/mapper/UserMapper.xml diff --git a/README-EN.md b/README-EN.md deleted file mode 100644 index 40e26e8..0000000 --- a/README-EN.md +++ /dev/null @@ -1,26 +0,0 @@ -# XNote - -[English](./README-EN.md) | [简体中文](./README.md) - -## Introduction - -Web notes based on markdown. - -## Technology Stack - -1. [Vue3](https://github.com/vuejs/) -2. [Nuxt3](https://nuxt.com/) -3. [TypeScript](https://www.typescriptlang.org/) -4. [Vditor](https://github.com/Vanessa219/vditor) -5. [Spring Boot](https://spring.io/guides/gs/spring-boot) -6. [MyBatis-Plus](https://baomidou.com) -7. [MySQL](https://www.mysql.com/) -8. [MinIO](https://min.io/) - -## Git Repositories - -- Frontend: [xnote-vue(github)](https://github.com/XCLHove/xnote-vue) | [xnote-vue(gitee)](https://gitee.com/xclhove/xnote-vue) - -- Frontend (Nuxt): [xnote-nuxt(github)](https://github.com/XCLHove/xnote-nuxt) | [xnote-nuxt(gitee)](https://gitee.com/xclhove/xnote-nuxt) - -- Backend: [xnote-springboot(github)](https://github.com/XCLHove/xnote-springboot) | [xnote-springboot(gitee)](https://gitee.com/xclhove/xnote-springboot) diff --git a/README.md b/README.md deleted file mode 100644 index 4a1d9fe..0000000 --- a/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# XNote - -[简体中文](./README.md) | [English](./README-EN.md) - -## 简介 - -基于markdown的Web笔记。 - -## 技术栈 - -1. [Vue3](https://github.com/vuejs/) -2. [Nuxt3](https://nuxt.com/) -3. [TypeScript](https://www.typescriptlang.org/) -4. [Vditor](https://github.com/Vanessa219/vditor) -5. [Spring Boot](https://spring.io/guides/gs/spring-boot) -6. [MyBatis-Plus](https://baomidou.com) -7. [MySQL](https://www.mysql.com/) -8. [MinIO](https://min.io/) - -## git仓库 - -- 前端:[xnote-vue(github)](https://github.com/XCLHove/xnote-vue) | [xnote-vue(gitee)](https://gitee.com/xclhove/xnote-vue) - -- 前端(Nuxt):[xnote-nuxt(github)](https://github.com/XCLHove/xnote-nuxt) | [xnote-nuxt(gitee)](https://gitee.com/xclhove/xnote-nuxt) - -- 后端:[xnote-springboot(github)](https://github.com/XCLHove/xnote-springboot) | [xnote-springboot(gitee)](https://gitee.com/xclhove/xnote-springboot) diff --git a/docs/api-doc.md b/docs/api-doc.md new file mode 100644 index 0000000..bb34057 --- /dev/null +++ b/docs/api-doc.md @@ -0,0 +1,2577 @@ +# XNote + +[简体中文](../readme.md) | [English](../readme-en.md) | api-doc + +Base URLs: + +* 开发环境: http://localhost:8080 + +# Authentication + +# 笔记相关接口 + +## POST 增加笔记 + +POST /note + +> Body 请求参数 + +```json +{ + "title": "string", + "content": "string", + "isPublic": "NO", + "typeId": 1 +} +``` + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|Authorization|header|string| 是 ||none| +|body|body|[NoteAddForm](#schemanoteaddform)| 否 | NoteAddForm|none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": 0 +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## DELETE 批量删除笔记 + +DELETE /note + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|noteIds|query|array[string]| 是 ||none| +|Authorization|header|string| 是 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": null +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## PUT 更新笔记 + +PUT /note + +> Body 请求参数 + +```json +{ + "id": 1, + "title": "string", + "content": "string", + "isPublic": "NO", + "typeId": 0 +} +``` + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|Authorization|header|string| 是 ||none| +|body|body|[NoteUpdateForm](#schemanoteupdateform)| 否 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": null +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## PUT 批量更新笔记类型 + +PUT /note/types + +> Body 请求参数 + +```json +{ + "noteIds": [ + 0 + ], + "typeId": 1 +} +``` + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|Authorization|header|string| 是 ||none| +|body|body|[NoteUpdateTypeForm](#schemanoteupdatetypeform)| 否 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": null +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## GET 查看笔记 + +GET /note/{noteId} + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|noteId|path|string| 是 ||none| +|shareCode|query|string| 否 ||none| +|Authorization|header|string| 是 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": { + "id": 0, + "title": "", + "content": "", + "userId": 0, + "releaseTime": "", + "updateTime": "", + "isPublic": "", + "typeId": 0 + } +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## GET 搜索笔记 + +GET /note/search + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|current|query|integer| 否 ||none| +|size|query|integer| 否 ||none| +|search|query|string| 否 ||none| +|heightLightPreTag|query|string| 否 ||none| +|heightLightPostTag|query|string| 否 ||none| +|Authorization|header|string| 是 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": { + "current": 0, + "page": 0, + "size": 0, + "total": 0, + "records": [ + { + "id": 0, + "title": "", + "content": "", + "userId": 0, + "releaseTime": "", + "updateTime": "", + "isPublic": "", + "typeId": 0 + } + ] + } +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## GET 搜索用户笔记 + +GET /note/search/{userId} + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|userId|path|string| 是 ||none| +|current|query|integer| 否 ||none| +|size|query|integer| 否 ||none| +|search|query|string| 否 ||none| +|typeId|query|integer| 否 ||none| +|Authorization|header|string| 是 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": { + "current": 0, + "page": 0, + "size": 0, + "total": 0, + "records": [ + { + "id": 0, + "title": "", + "userId": 0, + "releaseTime": "", + "isPublic": "", + "updateTime": "", + "typeId": 0 + } + ] + } +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +# 用户相关接口 + +## POST 注册 + +POST /user/register + +> Body 请求参数 + +```json +{ + "name": "string", + "account": "string", + "password": "string", + "email": "string", + "verificationCode": "string" +} +``` + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|Authorization|header|string| 是 ||none| +|body|body|[UserRegisterForm](#schemauserregisterform)| 否 | UserRegisterForm|none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": null +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## POST 登录 + +POST /user/login + +> Body 请求参数 + +```json +{ + "account": "string", + "password": "string" +} +``` + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|Authorization|header|string| 是 ||none| +|body|body|[UserLoginForm](#schemauserloginform)| 否 | UserLoginForm|none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": "" +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## POST 注销 + +POST /user/logout + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|Authorization|header|string| 是 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": "" +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## GET 根据id获取用户信息 + +GET /user/{userId} + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|userId|path|string| 是 ||none| +|Authorization|header|string| 是 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": { + "id": 0, + "name": "", + "account": "", + "email": "", + "status": "", + "homePageNoteId": 0, + "imageStorageSize": 0 + } +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## GET 获取自己的用户信息 + +GET /user/me + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|Authorization|header|string| 是 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": { + "id": 0, + "name": "", + "account": "", + "email": "", + "status": "", + "homePageNoteId": 0, + "imageStorageSize": 0 + } +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## GET 刷新token + +GET /user/new-token + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|Authorization|header|string| 是 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": "" +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## PUT 更新用户信息 + +PUT /user + +> Body 请求参数 + +```json +{ + "name": "string", + "homePageNoteId": 1 +} +``` + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|Authorization|header|string| 是 ||none| +|body|body|[UserUpdateForm](#schemauserupdateform)| 否 | UserUpdateForm|none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": null +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## PUT 更新密码 + +PUT /user/password + +> Body 请求参数 + +```json +{ + "password": "string" +} +``` + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|Authorization|header|string| 是 ||none| +|body|body|[UserUpdatePasswordForm](#schemauserupdatepasswordform)| 否 | UserUpdatePasswordForm|none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": null +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## PUT 更新邮箱 + +PUT /user/email + +> Body 请求参数 + +```json +{ + "email": "string", + "verificationCode": "string" +} +``` + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|Authorization|header|string| 是 ||none| +|body|body|[UserUpdateEmailForm](#schemauserupdateemailform)| 否 | UserUpdateEmailForm|none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": null +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +# 图片相关接口 + +## POST 上传图片 + +POST /image + +> Body 请求参数 + +```yaml +uploadImageFile: string + +``` + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|Authorization|header|string| 是 ||none| +|body|body|object| 否 ||none| +|» uploadImageFile|body|string(binary)| 是 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": "" +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## DELETE 删除图片 + +DELETE /image + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|userImageIds|query|array[string]| 是 ||none| +|Authorization|header|string| 是 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": null +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## GET 预览图片 + +GET /image/name/{imageName} + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|imageName|path|string| 是 ||none| +|Authorization|header|string| 是 ||none| + +> 返回示例 + +> 200 Response + +```json +{} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## GET 搜索自己的图片 + +GET /image/me + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|page|query|integer| 否 ||none| +|size|query|integer| 否 ||none| +|search|query|string| 否 ||none| +|Authorization|header|string| 是 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": { + "current": 0, + "page": 0, + "size": 0, + "total": 0, + "records": [ + { + "id": 0, + "userId": 0, + "imageId": 0, + "alias": "", + "lastDownloadTime": "" + } + ] + } +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +# 笔记分类相关接口 + +## POST 添加笔记分类 + +POST /note-type + +> Body 请求参数 + +```json +{ + "name": "string" +} +``` + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|Authorization|header|string| 是 ||none| +|body|body|[NoteTypeAddForm](#schemanotetypeaddform)| 否 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": null +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## DELETE 删除笔记分类 + +DELETE /note-type + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|ids|query|array[string]| 是 ||none| +|Authorization|header|string| 是 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": null +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## PUT 修改笔记分类 + +PUT /note-type + +> Body 请求参数 + +```json +{ + "id": 1, + "name": "string" +} +``` + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|Authorization|header|string| 是 ||none| +|body|body|[NoteTypeUpdateForm](#schemanotetypeupdateform)| 否 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": null +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## GET 获取用户所有笔记分类 + +GET /note-type/user/{userId} + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|userId|path|string| 是 ||none| +|Authorization|header|string| 是 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": [ + { + "id": 0, + "name": "", + "userId": 0 + } + ] +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +# 请求错误相关接口 + +## GET 请求路径不存在 + +GET /** + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|Authorization|header|string| 是 ||none| + +> 返回示例 + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +# 笔记分享记录相关接口 + +## POST 分享笔记 + +POST /share-note-record + +> Body 请求参数 + +```json +{ + "noteId": 1, + "expireTime": "new Timestamp(System.currentTimeMillis() + 1000 * 3600 * 24 * 7)" +} +``` + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|Authorization|header|string| 是 ||none| +|body|body|[ShareNoteRecordCreateForm](#schemasharenoterecordcreateform)| 否 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": "" +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## DELETE 批量删除分享的记录 + +DELETE /share-note-record + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|shareNoteRecordIds|query|array[string]| 是 ||none| +|Authorization|header|string| 是 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": null +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## PUT 修改笔记分享记录 + +PUT /share-note-record + +> Body 请求参数 + +```json +{ + "id": 0, + "expireTime": "new Timestamp(System.currentTimeMillis() + 1000 * 3600 * 24 * 7)" +} +``` + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|Authorization|header|string| 是 ||none| +|body|body|[ShareNoteRecordUpdateForm](#schemasharenoterecordupdateform)| 否 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": null +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## GET 获取分享的记录 + +GET /share-note-record/me + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|current|query|integer| 否 ||none| +|size|query|integer| 否 ||none| +|Authorization|header|string| 是 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": { + "current": 0, + "page": 0, + "size": 0, + "total": 0, + "records": [ + { + "id": 0, + "code": "", + "noteId": 0, + "userId": 0, + "expireTime": "", + "title": "" + } + ] + } +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +# 验证码相关接口 + +## GET 发送验证码到邮箱 + +GET /verification-code/send/to-email + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|email|query|string| 是 ||none| +|imageCode|query|string| 是 ||none| +|Authorization|header|string| 是 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": 0 +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## GET 获取图片验证码的base64 + +GET /verification-code/image/base64 + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|Authorization|header|string| 是 ||none| + +> 返回示例 + +```json +{ + "status": 0, + "message": "", + "data": "" +} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +## GET 获取图片验证码 + +GET /verification-code/image + +### 请求参数 + +|名称|位置|类型|必选|中文名|说明| +|---|---|---|---|---|---| +|Authorization|header|string| 是 ||none| + +> 返回示例 + +> 200 Response + +```json +{} +``` + +### 返回结果 + +|状态码|状态码含义|说明|数据模型| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline| + +### 返回数据结构 + +# 数据模型 + +

Result«User»

+ + + + + + +```json +{ + "status": 0, + "message": "string", + "data": { + "id": 0, + "name": "string", + "account": "string", + "email": "string", + "status": "DISABLE", + "homePageNoteId": 0, + "imageStorageSize": 0 + } +} + +``` + +Result«User» + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|status|integer¦null|false|none||none| +|message|string¦null|false|none||none| +|data|[User](#schemauser)|false|none||none| + +

User

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "account": "string", + "email": "string", + "status": "DISABLE", + "homePageNoteId": 0, + "imageStorageSize": 0 +} + +``` + +User + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|id|integer¦null|false|none||none| +|name|string¦null|false|none||none| +|account|string¦null|false|none||none| +|email|string¦null|false|none||none| +|status|string¦null|false|none||none| +|homePageNoteId|integer¦null|false|none||none| +|imageStorageSize|integer¦null|false|none||none| + +#### 枚举值 + +|属性|值| +|---|---| +|status|DISABLE| +|status|ENABLE| + +

Note

+ + + + + + +```json +{ + "id": 0, + "title": "string", + "content": "string", + "userId": 0, + "releaseTime": "string", + "updateTime": "string", + "isPublic": "NO", + "typeId": 0 +} + +``` + +Note + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|id|integer¦null|false|none||none| +|title|string¦null|false|none||none| +|content|string¦null|false|none||none| +|userId|integer¦null|false|none||none| +|releaseTime|string¦null|false|none||none| +|updateTime|string¦null|false|none||none| +|isPublic|string¦null|false|none||none| +|typeId|integer¦null|false|none||none| + +#### 枚举值 + +|属性|值| +|---|---| +|isPublic|NO| +|isPublic|YES| + +

Result«Note»

+ + + + + + +```json +{ + "status": 0, + "message": "string", + "data": { + "id": 0, + "title": "string", + "content": "string", + "userId": 0, + "releaseTime": "string", + "updateTime": "string", + "isPublic": "NO", + "typeId": 0 + } +} + +``` + +Result«Note» + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|status|integer¦null|false|none||none| +|message|string¦null|false|none||none| +|data|[Note](#schemanote)|false|none||none| + +

Image

+ + + + + + +```json +{ + "id": 0, + "userId": 0, + "alias": "string", + "name": "string", + "uploadTime": "string", + "url": "string", + "size": 0 +} + +``` + +Image + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|id|integer¦null|false|none||none| +|userId|integer¦null|false|none||none| +|alias|string¦null|false|none||none| +|name|string¦null|false|none||none| +|uploadTime|string¦null|false|none||none| +|url|string¦null|false|none||none| +|size|integer¦null|false|none||none| + +

NoteType

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "userId": 0 +} + +``` + +NoteType + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|id|integer¦null|false|none||none| +|name|string¦null|false|none||none| +|userId|integer¦null|false|none||none| + +

Result«List«NoteType»»

+ + + + + + +```json +{ + "status": 0, + "message": "string", + "data": [ + { + "id": 0, + "name": "string", + "userId": 0 + } + ] +} + +``` + +Result«List«NoteType»» + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|status|integer¦null|false|none||none| +|message|string¦null|false|none||none| +|data|[[NoteType](#schemanotetype)]¦null|false|none||none| + +

NoteAddForm

+ + + + + + +```json +{ + "title": "string", + "content": "string", + "isPublic": "NO", + "typeId": 1 +} + +``` + +NoteAddForm + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|title|string¦null|true|none||none| +|content|string¦null|true|none||none| +|isPublic|string¦null|true|none||none| +|typeId|integer¦null|false|none||none| + +#### 枚举值 + +|属性|值| +|---|---| +|isPublic|NO| +|isPublic|YES| + +

NoteShareForm

+ + + + + + +```json +{ + "noteId": 1, + "expireTime": "new Date(System.currentTimeMillis() + 1000 * 3600 * 24 * 7)" +} + +``` + +NoteShareForm + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|noteId|integer¦null|true|none||none| +|expireTime|string¦null|false|none||默认为7天后过期| + +

PageShareNoteVO

+ + + + + + +```json +{ + "id": 0, + "code": "string", + "noteId": 0, + "userId": 0, + "expireTime": "string", + "title": "string" +} + +``` + +PageShareNoteVO + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|id|integer¦null|false|none||none| +|code|string¦null|false|none||none| +|noteId|integer¦null|false|none||none| +|userId|integer¦null|false|none||none| +|expireTime|string¦null|false|none||none| +|title|string¦null|false|none||none| + +

PageVO«Image»

+ + + + + + +```json +{ + "current": 0, + "page": 0, + "size": 0, + "total": 0, + "records": [ + { + "id": 0, + "userId": 0, + "alias": "string", + "name": "string", + "uploadTime": "string", + "url": "string", + "size": 0 + } + ] +} + +``` + +PageVO«Image» + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|current|integer¦null|false|none||none| +|page|integer¦null|false|none||none| +|size|integer¦null|false|none||none| +|total|integer¦null|false|none||none| +|records|[[Image](#schemaimage)]¦null|false|none||none| + +

PageVO«PageShareNoteVO»

+ + + + + + +```json +{ + "current": 0, + "page": 0, + "size": 0, + "total": 0, + "records": [ + { + "id": 0, + "code": "string", + "noteId": 0, + "userId": 0, + "expireTime": "string", + "title": "string" + } + ] +} + +``` + +PageVO«PageShareNoteVO» + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|current|integer¦null|false|none||none| +|page|integer¦null|false|none||none| +|size|integer¦null|false|none||none| +|total|integer¦null|false|none||none| +|records|[[PageShareNoteVO](#schemapagesharenotevo)]¦null|false|none||none| + +

Result«PageVO«Image»»

+ + + + + + +```json +{ + "status": 0, + "message": "string", + "data": { + "current": 0, + "page": 0, + "size": 0, + "total": 0, + "records": [ + { + "id": 0, + "userId": 0, + "alias": "string", + "name": "string", + "uploadTime": "string", + "url": "string", + "size": 0 + } + ] + } +} + +``` + +Result«PageVO«Image»» + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|status|integer¦null|false|none||none| +|message|string¦null|false|none||none| +|data|[PageVO«Image»](#schemapagevo%c2%abimage%c2%bb)|false|none||none| + +

Result«PageVO«PageShareNoteVO»»

+ + + + + + +```json +{ + "status": 0, + "message": "string", + "data": { + "current": 0, + "page": 0, + "size": 0, + "total": 0, + "records": [ + { + "id": 0, + "code": "string", + "noteId": 0, + "userId": 0, + "expireTime": "string", + "title": "string" + } + ] + } +} + +``` + +Result«PageVO«PageShareNoteVO»» + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|status|integer¦null|false|none||none| +|message|string¦null|false|none||none| +|data|[PageVO«PageShareNoteVO»](#schemapagevo%c2%abpagesharenotevo%c2%bb)|false|none||none| + +

UserLoginForm

+ + + + + + +```json +{ + "account": "string", + "password": "string" +} + +``` + +UserLoginForm + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|account|string¦null|true|none||none| +|password|string¦null|true|none||none| + +

UserRegisterForm

+ + + + + + +```json +{ + "name": "string", + "account": "string", + "password": "string", + "email": "string", + "verificationCode": "string" +} + +``` + +UserRegisterForm + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|name|string¦null|true|none||none| +|account|string¦null|true|none||none| +|password|string¦null|true|none||none| +|email|string¦null|true|none||none| +|verificationCode|string¦null|true|none||none| + +

UserUpdateEmailForm

+ + + + + + +```json +{ + "email": "string", + "verificationCode": "string" +} + +``` + +UserUpdateEmailForm + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|email|string¦null|true|none||none| +|verificationCode|string¦null|true|none||none| + +

UserUpdateForm

+ + + + + + +```json +{ + "name": "string", + "homePageNoteId": 1 +} + +``` + +UserUpdateForm + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|name|string¦null|false|none||none| +|homePageNoteId|integer¦null|false|none||none| + +

UserUpdatePasswordForm

+ + + + + + +```json +{ + "password": "string" +} + +``` + +UserUpdatePasswordForm + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|password|string¦null|false|none||none| + +

Result«String»

+ + + + + + +```json +{ + "status": 0, + "message": "string", + "data": "string" +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|status|integer¦null|false|none||none| +|message|string¦null|false|none||none| +|data|string¦null|false|none||none| + +

Result«Object»

+ + + + + + +```json +{ + "status": null, + "message": null, + "data": null +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|status|null¦null|false|none||none| +|message|null¦null|false|none||none| +|data|null¦null|false|none||none| + +

Result«Integer»

+ + + + + + +```json +{ + "status": 0, + "message": "string", + "data": 0 +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|status|integer¦null|false|none||none| +|message|string¦null|false|none||none| +|data|integer¦null|false|none||none| + +

ShareNoteVO

+ + + + + + +```json +{ + "id": 0, + "title": "string", + "userId": 0, + "releaseTime": "string", + "isPublic": "NO", + "updateTime": "string", + "typeId": 0 +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|id|integer¦null|false|none||none| +|title|string¦null|false|none||none| +|userId|integer¦null|false|none||none| +|releaseTime|string¦null|false|none||none| +|isPublic|string¦null|false|none||none| +|updateTime|string¦null|false|none||none| +|typeId|integer¦null|false|none||none| + +#### 枚举值 + +|属性|值| +|---|---| +|isPublic|NO| +|isPublic|YES| + +

PageVO«ShareNoteVO»

+ + + + + + +```json +{ + "current": 0, + "page": 0, + "size": 0, + "total": 0, + "records": [ + { + "id": 0, + "title": "string", + "userId": 0, + "releaseTime": "string", + "isPublic": "NO", + "updateTime": "string", + "typeId": 0 + } + ] +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|current|integer¦null|false|none||none| +|page|integer¦null|false|none||none| +|size|integer¦null|false|none||none| +|total|integer¦null|false|none||none| +|records|[[ShareNoteVO](#schemasharenotevo)]¦null|false|none||none| + +

Result«PageVO«ShareNoteVO»»

+ + + + + + +```json +{ + "status": 0, + "message": "string", + "data": { + "current": 0, + "page": 0, + "size": 0, + "total": 0, + "records": [ + { + "id": 0, + "title": "string", + "userId": 0, + "releaseTime": "string", + "isPublic": "NO", + "updateTime": "string", + "typeId": 0 + } + ] + } +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|status|integer¦null|false|none||none| +|message|string¦null|false|none||none| +|data|[PageVO«ShareNoteVO»](#schemapagevo%c2%absharenotevo%c2%bb)|false|none||none| + +

NoteUpdateForm

+ + + + + + +```json +{ + "id": 1, + "title": "string", + "content": "string", + "isPublic": "NO", + "typeId": 0 +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|id|integer¦null|true|none||none| +|title|string¦null|false|none||none| +|content|string¦null|false|none||none| +|isPublic|string¦null|false|none||none| +|typeId|integer¦null|false|none||none| + +#### 枚举值 + +|属性|值| +|---|---| +|isPublic|NO| +|isPublic|YES| + +

Result«?»

+ + + + + + +```json +{ + "status": 0, + "message": "string", + "data": null +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|status|integer¦null|false|none||none| +|message|string¦null|false|none||none| +|data|null¦null|false|none||none| + +

SearchNoteVO

+ + + + + + +```json +{ + "id": 0, + "title": "string", + "userId": 0, + "releaseTime": "string", + "isPublic": "NO", + "updateTime": "string", + "typeId": 0 +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|id|integer¦null|false|none||none| +|title|string¦null|false|none||none| +|userId|integer¦null|false|none||none| +|releaseTime|string¦null|false|none||none| +|isPublic|string¦null|false|none||none| +|updateTime|string¦null|false|none||none| +|typeId|integer¦null|false|none||none| + +#### 枚举值 + +|属性|值| +|---|---| +|isPublic|NO| +|isPublic|YES| + +

PageVO«SearchNoteVO»

+ + + + + + +```json +{ + "current": 0, + "page": 0, + "size": 0, + "total": 0, + "records": [ + { + "id": 0, + "title": "string", + "userId": 0, + "releaseTime": "string", + "isPublic": "NO", + "updateTime": "string", + "typeId": 0 + } + ] +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|current|integer¦null|false|none||none| +|page|integer¦null|false|none||none| +|size|integer¦null|false|none||none| +|total|integer¦null|false|none||none| +|records|[[SearchNoteVO](#schemasearchnotevo)]¦null|false|none||none| + +

Result«PageVO«SearchNoteVO»»

+ + + + + + +```json +{ + "status": 0, + "message": "string", + "data": { + "current": 0, + "page": 0, + "size": 0, + "total": 0, + "records": [ + { + "id": 0, + "title": "string", + "userId": 0, + "releaseTime": "string", + "isPublic": "NO", + "updateTime": "string", + "typeId": 0 + } + ] + } +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|status|integer¦null|false|none||none| +|message|string¦null|false|none||none| +|data|[PageVO«SearchNoteVO»](#schemapagevo%c2%absearchnotevo%c2%bb)|false|none||none| + +

NoteUpdateTypeForm

+ + + + + + +```json +{ + "noteIds": [ + 0 + ], + "typeId": 1 +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|noteIds|[integer]¦null|true|none||none| +|typeId|integer¦null|true|none||none| + +

Result«Set«String»»

+ + + + + + +```json +{ + "status": 0, + "message": "string", + "data": [ + "string" + ] +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|status|integer¦null|false|none||none| +|message|string¦null|false|none||none| +|data|[string]¦null|false|none||none| + +

ShareNoteRecordCreateForm

+ + + + + + +```json +{ + "noteId": 1, + "expireTime": "new Timestamp(System.currentTimeMillis() + 1000 * 3600 * 24 * 7)" +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|noteId|integer¦null|true|none||none| +|expireTime|string¦null|false|none||默认为7天后过期| + +

ShareNoteRecordVO

+ + + + + + +```json +{ + "id": 0, + "code": "string", + "noteId": 0, + "userId": 0, + "expireTime": "string", + "title": "string" +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|id|integer¦null|false|none||none| +|code|string¦null|false|none||none| +|noteId|integer¦null|false|none||none| +|userId|integer¦null|false|none||none| +|expireTime|string¦null|false|none||none| +|title|string¦null|false|none||none| + +

PageVO«ShareNoteRecordVO»

+ + + + + + +```json +{ + "current": 0, + "page": 0, + "size": 0, + "total": 0, + "records": [ + { + "id": 0, + "code": "string", + "noteId": 0, + "userId": 0, + "expireTime": "string", + "title": "string" + } + ] +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|current|integer¦null|false|none||none| +|page|integer¦null|false|none||none| +|size|integer¦null|false|none||none| +|total|integer¦null|false|none||none| +|records|[[ShareNoteRecordVO](#schemasharenoterecordvo)]¦null|false|none||none| + +

Result«PageVO«ShareNoteRecordVO»»

+ + + + + + +```json +{ + "status": 0, + "message": "string", + "data": { + "current": 0, + "page": 0, + "size": 0, + "total": 0, + "records": [ + { + "id": 0, + "code": "string", + "noteId": 0, + "userId": 0, + "expireTime": "string", + "title": "string" + } + ] + } +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|status|integer¦null|false|none||none| +|message|string¦null|false|none||none| +|data|[PageVO«ShareNoteRecordVO»](#schemapagevo%c2%absharenoterecordvo%c2%bb)|false|none||none| + +

ShareNoteRecordUpdateForm

+ + + + + + +```json +{ + "id": 0, + "expireTime": "new Timestamp(System.currentTimeMillis() + 1000 * 3600 * 24 * 7)" +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|id|integer¦null|false|none||none| +|expireTime|string¦null|false|none||none| + +

SearchUserImageVO

+ + + + + + +```json +{ + "id": 0, + "userId": 0, + "imageId": 0, + "alias": "string", + "lastDownloadTime": "string" +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|id|integer¦null|false|none||none| +|userId|integer¦null|false|none||none| +|imageId|integer¦null|false|none||none| +|alias|string¦null|false|none||none| +|lastDownloadTime|string¦null|false|none||none| + +

PageVO«SearchUserImageVO»

+ + + + + + +```json +{ + "current": 0, + "page": 0, + "size": 0, + "total": 0, + "records": [ + { + "id": 0, + "userId": 0, + "imageId": 0, + "alias": "string", + "lastDownloadTime": "string" + } + ] +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|current|integer¦null|false|none||none| +|page|integer¦null|false|none||none| +|size|integer¦null|false|none||none| +|total|integer¦null|false|none||none| +|records|[[SearchUserImageVO](#schemasearchuserimagevo)]¦null|false|none||none| + +

Result«PageVO«SearchUserImageVO»»

+ + + + + + +```json +{ + "status": 0, + "message": "string", + "data": { + "current": 0, + "page": 0, + "size": 0, + "total": 0, + "records": [ + { + "id": 0, + "userId": 0, + "imageId": 0, + "alias": "string", + "lastDownloadTime": "string" + } + ] + } +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|status|integer¦null|false|none||none| +|message|string¦null|false|none||none| +|data|[PageVO«SearchUserImageVO»](#schemapagevo%c2%absearchuserimagevo%c2%bb)|false|none||none| + +

PageVO«Note»

+ + + + + + +```json +{ + "current": 0, + "page": 0, + "size": 0, + "total": 0, + "records": [ + { + "id": 0, + "title": "string", + "content": "string", + "userId": 0, + "releaseTime": "string", + "updateTime": "string", + "isPublic": "NO", + "typeId": 0 + } + ] +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|current|integer¦null|false|none||none| +|page|integer¦null|false|none||none| +|size|integer¦null|false|none||none| +|total|integer¦null|false|none||none| +|records|[[Note](#schemanote)]¦null|false|none||none| + +

Result«PageVO«Note»»

+ + + + + + +```json +{ + "status": 0, + "message": "string", + "data": { + "current": 0, + "page": 0, + "size": 0, + "total": 0, + "records": [ + { + "id": 0, + "title": "string", + "content": "string", + "userId": 0, + "releaseTime": "string", + "updateTime": "string", + "isPublic": "NO", + "typeId": 0 + } + ] + } +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|status|integer¦null|false|none||none| +|message|string¦null|false|none||none| +|data|[PageVO«Note»](#schemapagevo%c2%abnote%c2%bb)|false|none||none| + +

NoteTypeAddForm

+ + + + + + +```json +{ + "name": "string" +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|name|string¦null|true|none||none| + +

NoteTypeUpdateForm

+ + + + + + +```json +{ + "id": 1, + "name": "string" +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|id|integer¦null|true|none||none| +|name|string¦null|true|none||none| + diff --git a/docs/images/database.png b/docs/images/database.png new file mode 100644 index 0000000000000000000000000000000000000000..00557dd57e648541b42b4bbe13fc963898a21142 GIT binary patch literal 269139 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
SoJ$kI=|!#*`Pz2k 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

j|Bsc@25&S!})p9sMpO<000 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 z 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�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.0.0 com.xclhove xnote-springboot - 1.0.0-SNAPSHOT - xnote - xnote + v2.0.0 + XNote 1.8 @@ -16,6 +15,26 @@ UTF-8 2.6.13 + + + + + dev + + dev + + + true + + + + + release + + release + + + @@ -34,11 +53,6 @@ org.springframework.boot spring-boot-starter-validation - - - org.springframework.boot - spring-boot-starter-thymeleaf - com.baomidou @@ -49,6 +63,7 @@ com.mysql mysql-connector-j + 8.2.0 runtime @@ -61,7 +76,7 @@ cn.hutool hutool-all - 5.8.18 + 5.8.25 @@ -80,23 +95,11 @@ java-jwt 3.10.1 - - - io.springfox - springfox-swagger-ui - 2.9.2 - - - - io.springfox - springfox-swagger2 - 2.9.2 - io.minio minio - 8.4.3 + 8.5.11 com.squareup.okhttp3 @@ -104,39 +107,32 @@ - + com.squareup.okhttp3 okhttp 4.9.0 - - - - - - - - - org.springframework - spring-context-support - org.springframework.boot spring-boot-starter-data-redis - + - redis.clients - jedis + org.apache.commons + commons-pool2 - + org.springframework.boot spring-boot-starter-mail - + + + org.springframework.boot + spring-boot-starter-data-elasticsearch + @@ -168,7 +164,7 @@ spring-boot-maven-plugin ${spring-boot.version} - com.xclhove.xnote.XnoteApplication + com.xclhove.xnote.Application @@ -180,6 +176,27 @@ + + + + src/main/resources + + true + + + application-dev.yml + application-release.yml + + + + src/main/resources + true + + + application-${profileActive}.yml + + + diff --git a/readme-en.md b/readme-en.md new file mode 100644 index 0000000..c8261a1 --- /dev/null +++ b/readme-en.md @@ -0,0 +1,77 @@ +# XNote + +[简体中文](readme.md) | English | [api-doc](./docs/api-doc.md) + +## Introduction + +XNote is an open-source web-based Markdown note management application that supports online editing and previewing of Markdown notes. + +--- + +## Technology Stack + +1. [Vue3](https://github.com/vuejs/) +2. [Nuxt3](https://nuxt.com/) +3. [TypeScript](https://www.typescriptlang.org/) +4. [Vditor](https://github.com/Vanessa219/vditor) +5. [Spring Boot](https://spring.io/guides/gs/spring-boot) +6. [MyBatis-Plus](https://baomidou.com) +7. [MySQL](https://www.mysql.com/) +8. [MinIO](https://min.io/) +9. [Elasticsearch](https://www.elastic.co/cn/elasticsearch/) + +--- + +## Database Design + +![Database Design](./docs/images/database.png) + +--- + +## Git Repositories + +- Frontend: [xnote-nuxt(github)](https://github.com/XCLHove/xnote-nuxt) | [xnote-nuxt(gitee)](https://gitee.com/xclhove/xnote-nuxt) + +- Backend: [xnote-springboot(github)](https://github.com/XCLHove/xnote-springboot) | [xnote-springboot(gitee)](https://gitee.com/xclhove/xnote-springboot) + +--- + +## Backend Build + +Java 8 and Maven are required. + +1. Navigate to the project root directory and install dependencies: + ```sh + mvn clean install + ``` +2. Build + ```sh + mvn package + ``` +3. The build artifacts are located in the `.output` directory and can be directly deployed to the server: + ```sh + java -jar target/xnote-springboot-v.jar + ``` + +--- + +## Frontend Build + +Node.js and Yarn are required. + +1. Navigate to the project root directory and install dependencies: + ```sh + yarn install + ``` +2. Build + ```sh + yarn build + ``` +3. The build artifacts are located in the `.output` directory and can be directly deployed to the server. Start the server with `.output/server/index.mjs`: + ```sh + node ./output/server/index.mjs + ``` + +## Environment Variables(Frontend) +* API_SERVER_URL (required): Backend API address, e.g., "http://localhost:8080". +* IPC (optional): Record number, e.g., "蜀IPC备-xxxxxxx号". \ No newline at end of file diff --git a/readme.md b/readme.md index a62f270..bbd034e 100644 --- a/readme.md +++ b/readme.md @@ -1,19 +1,75 @@ # XNote +简体中文 | [English](readme-en.md) | [api-doc](./docs/api-doc.md) + ## 简介 -基于markdown的Web笔记。 +XNote 是一个开源 Web 端的 Markdown 笔记管理应用,支持在线编辑和预览 Markdown 笔记。 + +--- ## 技术栈 -1. Vue3 -2. TypeScript -3. Springboot -4. MySQL -5. MinIO -6. Redis +1. [Vue3](https://github.com/vuejs/) +2. [Nuxt3](https://nuxt.com/) +3. [TypeScript](https://www.typescriptlang.org/) +4. [Vditor](https://github.com/Vanessa219/vditor) +5. [Spring Boot](https://spring.io/guides/gs/spring-boot) +6. [MyBatis-Plus](https://baomidou.com) +7. [MySQL](https://www.mysql.com/) +8. [MinIO](https://min.io/) +9. [Elasticsearch](https://www.elastic.co/cn/elasticsearch/) + +--- + +## 数据库表 + +![数据库设计](./docs/images/database.png) ## git仓库 -- 前端:[xnote-vue(github)](https://github.com/XCLHove/xnote-vue) | [xnote-vue(gitee)](https://gitee.com/xclhove/xnote-vue) -- 后端:[xnote-springboot(github)](https://github.com/XCLHove/xnote-springboot) | [xnote-springboot(gitee)](https://gitee.com/xclhove/xnote-springboot) \ No newline at end of file +- 前端:[xnote-nuxt(github)](https://github.com/XCLHove/xnote-nuxt) | [xnote-nuxt(gitee)](https://gitee.com/xclhove/xnote-nuxt) + +- 后端:[xnote-springboot(github)](https://github.com/XCLHove/xnote-springboot) | [xnote-springboot(gitee)](https://gitee.com/xclhove/xnote-springboot) + +--- + +## 构建(后端) + +需要安装 JDK 8 和 Maven。 +1. 进入项目根目录,安装依赖: + ```sh + mvn clean install + ``` +2. 构建 + ```sh + mvn package + ``` +3. 构建产物在`target`目录下,可直接部署到服务器: + ```sh + java -jar target/xnote-springboot-v.jar + ``` + +--- + +## 构建(前端) + +需要安装 Node.js 和 Yarn。 + +1. 进入项目根目录,安装依赖: + ```sh + yarn install + ``` +2. 构建 + ```sh + yarn build + ``` +3. 构建产物在`.output`目录下,可直接部署到服务器,启动`.output/server/index.mjs`即可: + ```sh + node ./output/server/index.mjs + ``` + +## 环境变量(前端) + +* `API_SERVER_URL`(必填):后端 API 地址,如 “http://localhost:8080”。 +* `IPC`(可选): 备案号,如:“蜀IPC备-xxxxxxx号”。 \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/XnoteApplication.java b/src/main/java/com/xclhove/xnote/Application.java similarity index 59% rename from src/main/java/com/xclhove/xnote/XnoteApplication.java rename to src/main/java/com/xclhove/xnote/Application.java index 7d41d16..7cf301c 100644 --- a/src/main/java/com/xclhove/xnote/XnoteApplication.java +++ b/src/main/java/com/xclhove/xnote/Application.java @@ -1,5 +1,6 @@ package com.xclhove.xnote; +import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -7,10 +8,11 @@ * @author xclhove */ @SpringBootApplication -public class XnoteApplication { +@MapperScan("com.xclhove.xnote.mapper") +public class Application { public static void main(String[] args) { - SpringApplication.run(XnoteApplication.class, args); + SpringApplication.run(Application.class, args); } } diff --git a/src/main/java/com/xclhove/xnote/Interceptor/AdminJwtInterceptor.java b/src/main/java/com/xclhove/xnote/Interceptor/AdminJwtInterceptor.java deleted file mode 100644 index 57d744a..0000000 --- a/src/main/java/com/xclhove/xnote/Interceptor/AdminJwtInterceptor.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.xclhove.xnote.Interceptor; - -import cn.hutool.core.util.StrUtil; -import com.baomidou.mybatisplus.extension.toolkit.Db; -import com.xclhove.xnote.entity.table.Admin; -import com.xclhove.xnote.exception.AdminTokenException; -import com.xclhove.xnote.exception.ServiceException; -import com.xclhove.xnote.util.ThreadLocalUtil; -import com.xclhove.xnote.util.TokenUtil; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.web.method.HandlerMethod; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * @author xclhove - */ -@Slf4j -@Component -@RequiredArgsConstructor -public class AdminJwtInterceptor extends ServiceInterceptor { - @Target(ElementType.METHOD) - @Retention(RetentionPolicy.RUNTIME) - public @interface AdminJwtIntercept { - } - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - try { - //如果不是映射到方法直接通过 - if (!(handler instanceof HandlerMethod)) return true; - - HandlerMethod handlerMethod = (HandlerMethod) handler; - AdminJwtIntercept loginIntercept = handlerMethod.getMethod().getAnnotation(AdminJwtIntercept.class); - //如果要访问的方法上没有加这个注解,那么就说明这个方法不需要拦截,否则就需要进行拦截 - if (null == loginIntercept) return true; - - String token = request.getHeader("token"); - if (StrUtil.isBlank(token)) throw new AdminTokenException("管理员未登录!"); - Integer id = TokenUtil.getId(token); - Admin admin = Db.getById(id, Admin.class); - String password = admin.getPassword(); - //校验token - if (!TokenUtil.validate(token, password)) throw new AdminTokenException("token校验未通过!"); - ThreadLocalUtil.set("id", id); - return true; - } catch (ServiceException serviceException) { - throw serviceException; - } catch (Exception e) { - log.error(e.getMessage()); - throw new AdminTokenException("出现异常,token校验未通过!"); - } - } -} diff --git a/src/main/java/com/xclhove/xnote/Interceptor/DeviceInterceptor.java b/src/main/java/com/xclhove/xnote/Interceptor/DeviceInterceptor.java deleted file mode 100644 index fbb8964..0000000 --- a/src/main/java/com/xclhove/xnote/Interceptor/DeviceInterceptor.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.xclhove.xnote.Interceptor; - -import cn.hutool.crypto.SecureUtil; -import com.xclhove.xnote.constant.RequestHeaderKey; -import com.xclhove.xnote.constant.TreadLocalKey; -import com.xclhove.xnote.util.ThreadLocalUtil; -import lombok.RequiredArgsConstructor; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * @author xclhove - */ -@Component -@RequiredArgsConstructor -@Order(0) -public class DeviceInterceptor extends ServiceInterceptor{ - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - String userAgent = request.getHeader(RequestHeaderKey.USER_AGENT); - String deviceId = SecureUtil.md5(userAgent); - ThreadLocalUtil.set(TreadLocalKey.DEVICE_ID, deviceId); - return true; - } -} diff --git a/src/main/java/com/xclhove/xnote/Interceptor/IPInterceptor.java b/src/main/java/com/xclhove/xnote/Interceptor/IPInterceptor.java deleted file mode 100644 index 5267993..0000000 --- a/src/main/java/com/xclhove/xnote/Interceptor/IPInterceptor.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.xclhove.xnote.Interceptor; - -import com.xclhove.xnote.constant.RedisKey; -import com.xclhove.xnote.constant.TreadLocalKey; -import com.xclhove.xnote.exception.IpFrequencyException; -import com.xclhove.xnote.tool.RedisTool; -import com.xclhove.xnote.util.RequestUtil; -import com.xclhove.xnote.util.ThreadLocalUtil; -import com.xclhove.xnote.util.TokenUtil; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import org.springframework.web.method.HandlerMethod; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import java.util.concurrent.TimeUnit; - -/** - * ip拦截器 - * - * @author xclhove - */ -@Slf4j -@Component -@RequiredArgsConstructor -public class IPInterceptor extends ServiceInterceptor { - private final RedisTool redisTool; - @Value("${xnote.debug.enable: false}") - private boolean isDebug; - @Target(ElementType.METHOD) - @Retention(RetentionPolicy.RUNTIME) - public @interface UnlockIpFrequencyLimit { - } - - /** 单个ip每分钟最大请求次数 */ - @Value("${xnote.ip.max-frequency-per-minute}") - private int maxFrequencyPerMinute; - /** 单个ip访问频率异常提醒次数,如果超过提醒次数就不再返回任何信息 */ - final int promptNumber = 5; - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - String ip = RequestUtil.getIpAddr(request); - ThreadLocalUtil.set(TreadLocalKey.CLIENT_IP, ip); - - if (isDebug) return true; - - //如果不是映射到方法 - if (!(handler instanceof HandlerMethod)) return true; - - //方法上有加这个注解,那么就说明这个方法不需要拦截 - HandlerMethod handlerMethod = (HandlerMethod) handler; - UnlockIpFrequencyLimit unlockIpFrequencyLimit = handlerMethod.getMethod().getAnnotation(UnlockIpFrequencyLimit.class); - if (null != unlockIpFrequencyLimit) return true; - - int frequency = 0; - - Integer value = redisTool.getValue(RedisKey.IP_FREQUENCY + ip, Integer.class); - frequency = (value == null || value < 0) ? 0 : value; - - redisTool.setValue(RedisKey.IP_FREQUENCY + ip, frequency + 1, 1, TimeUnit.MINUTES); - - if (frequency > (maxFrequencyPerMinute + promptNumber)) { - return false; - } - if (frequency > maxFrequencyPerMinute) { - String logMessage = "ip_limit:" + ip; - String token = request.getHeader("token"); - Integer id = TokenUtil.getId(token); - if (id != null) logMessage += "--id:" + id; - log.error(logMessage); - throw new IpFrequencyException(); - } - return true; - } -} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/Interceptor/UserJwtInterceptor.java b/src/main/java/com/xclhove/xnote/Interceptor/UserJwtInterceptor.java deleted file mode 100644 index 6f39c3b..0000000 --- a/src/main/java/com/xclhove/xnote/Interceptor/UserJwtInterceptor.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.xclhove.xnote.Interceptor; - -import cn.hutool.core.util.StrUtil; -import com.baomidou.mybatisplus.extension.toolkit.Db; -import com.xclhove.xnote.constant.RequestHeaderKey; -import com.xclhove.xnote.constant.TreadLocalKey; -import com.xclhove.xnote.entity.table.User; -import com.xclhove.xnote.enums.entityattribute.UserStatus; -import com.xclhove.xnote.exception.ServiceException; -import com.xclhove.xnote.exception.UserTokenException; -import com.xclhove.xnote.tool.TokenTool; -import com.xclhove.xnote.util.ThreadLocalUtil; -import com.xclhove.xnote.util.TokenUtil; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.web.method.HandlerMethod; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * @author xclhove - */ -@Slf4j -@Component -@RequiredArgsConstructor -public class UserJwtInterceptor extends ServiceInterceptor { - @Target(ElementType.METHOD) - @Retention(RetentionPolicy.RUNTIME) - public @interface UserJwtIntercept { - } - - private final TokenTool tokenTool; - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - try { - //如果不是映射到方法直接通过 - if (!(handler instanceof HandlerMethod)) { - return true; - } - - HandlerMethod handlerMethod = (HandlerMethod) handler; - UserJwtIntercept loginIntercept = handlerMethod.getMethod().getAnnotation(UserJwtIntercept.class); - //如果要访问的方法上没有加这个注解,那么就说明这个方法不需要拦截,否则就需要进行拦截 - if(null == loginIntercept) { - return true; - } - - String token = request.getHeader(RequestHeaderKey.TOKEN); - // token为空则未登录 - if (StrUtil.isBlank(token)) { - throw new UserTokenException("未登录!"); - } - - Integer id = TokenUtil.getId(token); - User user = Db.getById(id, User.class); - // 检查是否被禁封 - if (user.getStatus() == UserStatus.BANED) { - throw new UserTokenException("用户已被禁封!"); - } - - //校验token - String password = user.getPassword(); - if (!TokenUtil.validate(token, password)) { - throw new UserTokenException("token校验未通过!"); - } - - // 和redis中的token进行对比 - String tokenInRedis = tokenTool.get(user.getId()); - if ((StrUtil.isBlank(tokenInRedis)) || (!token.equals(tokenInRedis))) { - throw new UserTokenException("登录过期,请重新登录!"); - } - - ThreadLocalUtil.set(TreadLocalKey.ID, id); - return true; - } catch (ServiceException serviceException) { - throw serviceException; - } - catch (Exception e) { - log.error(e.getMessage()); - throw new UserTokenException("出现异常,token校验未通过!"); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/Interceptor/validator/UserJwtValidator.java b/src/main/java/com/xclhove/xnote/Interceptor/validator/UserJwtValidator.java deleted file mode 100644 index 0ff8c28..0000000 --- a/src/main/java/com/xclhove/xnote/Interceptor/validator/UserJwtValidator.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.xclhove.xnote.Interceptor.validator; - -import cn.hutool.core.util.StrUtil; -import com.baomidou.mybatisplus.extension.toolkit.Db; -import com.xclhove.xnote.Interceptor.ServiceInterceptor; -import com.xclhove.xnote.constant.RequestHeaderKey; -import com.xclhove.xnote.constant.TreadLocalKey; -import com.xclhove.xnote.entity.table.User; -import com.xclhove.xnote.enums.entityattribute.UserStatus; -import com.xclhove.xnote.tool.TokenTool; -import com.xclhove.xnote.util.ThreadLocalUtil; -import com.xclhove.xnote.util.TokenUtil; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.web.method.HandlerMethod; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * @author xclhove - */ -@Slf4j -@Component -@RequiredArgsConstructor -public class UserJwtValidator extends ServiceInterceptor { - @Target(ElementType.METHOD) - @Retention(RetentionPolicy.RUNTIME) - public @interface UserJwtValidate { - } - - private final TokenTool tokenTool; - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - try { - //如果不是映射到方法直接通过 - if (!(handler instanceof HandlerMethod)) { - return true; - } - - HandlerMethod handlerMethod = (HandlerMethod) handler; - UserJwtValidate loginIntercept = handlerMethod.getMethod().getAnnotation(UserJwtValidate.class); - //如果要访问的方法上没有加这个注解,那么就说明这个方法不需要拦截,否则就需要进行拦截 - if(null == loginIntercept) { - return true; - } - - String token = request.getHeader(RequestHeaderKey.TOKEN); - if (StrUtil.isBlank(token)) { - ThreadLocalUtil.set(TreadLocalKey.ID, null); - return true; - } - Integer id = TokenUtil.getId(token); - User user = Db.getById(id, User.class); - if (user.getStatus() == UserStatus.BANED) { - ThreadLocalUtil.set(TreadLocalKey.ID, null); - return true; - } - String tokenInRedis = tokenTool.get(user.getId()); - if ((StrUtil.isBlank(tokenInRedis)) || (!token.equals(tokenInRedis))) { - ThreadLocalUtil.set(TreadLocalKey.ID, null); - return true; - } - String password = user.getPassword(); - //校验token - if (!TokenUtil.validate(token, password)) { - ThreadLocalUtil.set(TreadLocalKey.ID, null); - return true; - } - ThreadLocalUtil.set(TreadLocalKey.ID, id); - return true; - } catch (Exception e) { - log.error(e.getMessage()); - ThreadLocalUtil.set(TreadLocalKey.ID, null); - return true; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/config/GlobalExceptionHandler.java b/src/main/java/com/xclhove/xnote/config/GlobalExceptionHandler.java index 3fb4d31..5f142cb 100644 --- a/src/main/java/com/xclhove/xnote/config/GlobalExceptionHandler.java +++ b/src/main/java/com/xclhove/xnote/config/GlobalExceptionHandler.java @@ -1,41 +1,118 @@ package com.xclhove.xnote.config; import cn.hutool.core.util.StrUtil; -import com.xclhove.xnote.enums.result.ResultType; +import com.xclhove.xnote.constant.RedisKey; +import com.xclhove.xnote.exception.AbstractServiceException; import com.xclhove.xnote.exception.ParameterValidateException; -import com.xclhove.xnote.exception.ServiceException; -import com.xclhove.xnote.util.Result; -import org.springframework.web.bind.annotation.ControllerAdvice; +import com.xclhove.xnote.exception.SystemException; +import com.xclhove.xnote.tool.EmailTool; +import com.xclhove.xnote.tool.Result; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import javax.mail.MessagingException; import javax.servlet.http.HttpServletResponse; +import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; +import java.util.List; +import java.util.concurrent.TimeUnit; /** - * 全局异常配置 + * 全局异常处理 + * ExceptionHandler相当于controller的@RequestMapping * * @author xclhove */ -@ControllerAdvice +@Slf4j +@RestControllerAdvice +@RequiredArgsConstructor public class GlobalExceptionHandler { + private final EmailTool emailTool; + private final JavaMailConfig javaMailConfig; + private final StringRedisTemplate stringRedisTemplate; + /** - * ExceptionHandler相当于controller的@RequestMapping,如果抛出的的是ServiceException,则调用该方法 - * - * @param exception 异常 - * @return 异常信息 + * 业务异常 */ - @ExceptionHandler(ServiceException.class) - @ResponseBody - public Result serviceHandle(HttpServletResponse response, ServiceException exception) { + @ExceptionHandler(AbstractServiceException.class) + public Result serviceExceptionHandle(HttpServletResponse response, AbstractServiceException exception) { response.setContentType("application/json"); - return Result.error(exception.getStatus(), exception.getMessage(), exception.getData()); + return Result.fail(exception.getStatus(), exception.getMessage(), exception.getData()); + } + + /** + * 方法参数校验异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public Result methodArgumentNotValidExceptionHandler(HttpServletResponse response, MethodArgumentNotValidException exception) { + List allErrors = exception.getBindingResult().getAllErrors(); + return serviceExceptionHandle(response, new ParameterValidateException(allErrors.get(0).getDefaultMessage())); + } + + /** + * 方法参数类型不匹配 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public Result methodArgumentTypeMismatchExceptionHandler(HttpServletResponse response, MethodArgumentTypeMismatchException exception) { + return serviceExceptionHandle(response, new ParameterValidateException("参数类型不匹配:" + exception.getName())); } + /** + * 参数校验异常 + */ @ExceptionHandler(ConstraintViolationException.class) - @ResponseBody - public Result constraintViolationHandle(HttpServletResponse response, ConstraintViolationException exception) { - String message = StrUtil.isNotBlank(exception.getMessage()) ? exception.getMessage() : ResultType.PARAMETER_VALIDATE_EXCEPTION.getMessage(); - return serviceHandle(response, new ParameterValidateException(message)); + public Result constraintViolationExceptionHandler(HttpServletResponse response, ConstraintViolationException exception) { + String message = exception.getConstraintViolations().stream().map(ConstraintViolation::getMessage).findFirst().orElse(""); + if (StrUtil.isBlank(message)) { + return serviceExceptionHandle(response, new ParameterValidateException()); + } + return serviceExceptionHandle(response, new ParameterValidateException(message)); + } + + /** + * 缺少请求参数异常 + */ + @ExceptionHandler(MissingServletRequestParameterException.class) + public Result missingServletRequestParameterExceptionHandler(HttpServletResponse response, MissingServletRequestParameterException exception) { + String message = "缺少请求参数:" + exception.getParameterName(); + return serviceExceptionHandle(response, new ParameterValidateException(message)); + } + + /** + * JSON格式异常 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + public Result jsonParseExceptionHandler(HttpServletResponse response, HttpMessageNotReadableException exception) { + return serviceExceptionHandle(response, new SystemException("JSON格式错误")); + } + + /** + * 系统(未知)异常 + */ + @ExceptionHandler(Exception.class) + public Result exceptionHandler(HttpServletResponse response, Exception exception) { + String message = String.join(":", "系统异常", exception.getMessage()); + + String redisKey = RedisKey.Exception.CAN_SEND; + Long expire = stringRedisTemplate.getExpire(redisKey); + if (expire == null || expire == -1) { + try { + emailTool.sendMail(javaMailConfig.getUsername(), "XNote出现未知异常", message); + } catch (MessagingException e) { + log.error("邮件发送失败", e); + } + stringRedisTemplate.opsForValue().set(redisKey, "false", 30, TimeUnit.MINUTES); + } + + log.error(message, exception); + return serviceExceptionHandle(response, new SystemException()); } } diff --git a/src/main/java/com/xclhove/xnote/config/InterceptorConfig.java b/src/main/java/com/xclhove/xnote/config/InterceptorConfig.java index d00e45e..1f7515d 100644 --- a/src/main/java/com/xclhove/xnote/config/InterceptorConfig.java +++ b/src/main/java/com/xclhove/xnote/config/InterceptorConfig.java @@ -1,6 +1,6 @@ package com.xclhove.xnote.config; -import com.xclhove.xnote.Interceptor.ServiceInterceptor; +import com.xclhove.xnote.interceptor.ServiceInterceptor; import com.xclhove.xnote.util.SubclassFinder; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.config.BeanDefinition; @@ -23,15 +23,17 @@ public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { - // 获取ServiceInterceptor的子类 + // 获取 ServiceInterceptor 的子类拦截器 Set subclasses = SubclassFinder.findSubclasses(ServiceInterceptor.class); - // 将ServiceInterceptor的子类注册到拦截器 - subclasses.forEach(c -> { + // 注册子类拦截器 + subclasses.forEach(beanDefinition -> { try { - ServiceInterceptor interceptor = (ServiceInterceptor) applicationContext.getBean(Class.forName(c.getBeanClassName())); + String className = beanDefinition.getBeanClassName(); + Class intercepterClass = (Class) Class.forName(className); + ServiceInterceptor interceptor = applicationContext.getBean(intercepterClass); registry.addInterceptor(interceptor); - } catch (Exception e) { + } catch (ClassNotFoundException e) { throw new RuntimeException(e); } }); diff --git a/src/main/java/com/xclhove/xnote/config/MailConfig.java b/src/main/java/com/xclhove/xnote/config/JavaMailConfig.java similarity index 91% rename from src/main/java/com/xclhove/xnote/config/MailConfig.java rename to src/main/java/com/xclhove/xnote/config/JavaMailConfig.java index 119d50e..09df277 100644 --- a/src/main/java/com/xclhove/xnote/config/MailConfig.java +++ b/src/main/java/com/xclhove/xnote/config/JavaMailConfig.java @@ -12,12 +12,9 @@ @Configuration @ConfigurationProperties(prefix = "spring.mail") @Data -public class MailConfig { +public class JavaMailConfig { private String host; - private String username; - private String password; - private int port; } \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/config/MinioConfig.java b/src/main/java/com/xclhove/xnote/config/MinioConfig.java index ef15a8a..d22509f 100755 --- a/src/main/java/com/xclhove/xnote/config/MinioConfig.java +++ b/src/main/java/com/xclhove/xnote/config/MinioConfig.java @@ -15,11 +15,26 @@ @Configuration @ConfigurationProperties(prefix = "minio") public class MinioConfig { - + /** + * Minio服务所在地址 + */ private String endpoint; + /** + * Minio服务外网访问地址 + */ + private String remoteEndpoint; + /** + * 存储桶名称 + */ + private String bucketName; + /** + * 访问的key + */ private String accessKey; + /** + * 访问的秘钥 + */ private String secretKey; - private String bucketName; @Bean public MinioClient minioClient() { diff --git a/src/main/java/com/xclhove/xnote/config/MybatisPlusConfig.java b/src/main/java/com/xclhove/xnote/config/MybatisPlusConfig.java index 8eede63..6ecb6cf 100755 --- a/src/main/java/com/xclhove/xnote/config/MybatisPlusConfig.java +++ b/src/main/java/com/xclhove/xnote/config/MybatisPlusConfig.java @@ -3,8 +3,10 @@ import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import com.xclhove.xnote.mybatisplus.MybatisPlusPrintSqlInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; /** * MybatisPlus配置 @@ -19,4 +21,10 @@ public MybatisPlusInterceptor mybatisPlusInterceptor() { interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } + + @Bean + @Profile("dev") + public MybatisPlusPrintSqlInterceptor mybatisPlusPrintSqlInterceptor() { + return new MybatisPlusPrintSqlInterceptor(); + } } \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/config/RedisConfig.java b/src/main/java/com/xclhove/xnote/config/RedisConfig.java deleted file mode 100644 index 37e1b0d..0000000 --- a/src/main/java/com/xclhove/xnote/config/RedisConfig.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.xclhove.xnote.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.cache.annotation.EnableCaching; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisPassword; -import org.springframework.data.redis.connection.RedisStandaloneConfiguration; -import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; - -/** - * Redis配置类 - * - * @author xclhove - */ -@Configuration -@EnableCaching -@ConfigurationProperties(prefix = "spring.redis") -@Data -public class RedisConfig { - - private String host; - - private int port; - - private String password; - - private int database; - - @Bean - public JedisConnectionFactory jedisConnectionFactory() { - RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port); - config.setPassword(RedisPassword.of(password)); - config.setDatabase(database); - return new JedisConnectionFactory(config); - } - - @Bean - public RedisTemplate redisTemplate() { - RedisTemplate template = new RedisTemplate<>(); - template.setConnectionFactory(jedisConnectionFactory()); - return template; - } -} diff --git a/src/main/java/com/xclhove/xnote/config/RunnerConfig.java b/src/main/java/com/xclhove/xnote/config/RunnerConfig.java deleted file mode 100644 index 32198af..0000000 --- a/src/main/java/com/xclhove/xnote/config/RunnerConfig.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.xclhove.xnote.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -/** - * @author xclhove - * runner配置 - */ -@Configuration -@ConfigurationProperties(prefix = "xnote.runner") -@Data -public class RunnerConfig { - /** - * 是否需要替换笔记中的图片地址 - */ - private Boolean enableImageUrlReplaceRunner = false; - /** - * 是否在启动后检查Minio的状态 - */ - private Boolean enableMinioStatusCheckRunner = true; - /** - * 是否在启动后检查Redis的状态 - */ - private Boolean enableRedisStatusCheckRunner = true; - /** - * 是否需要对笔记中的关键字存储格式进行转换 - */ - private Boolean enableKeywordsConvertRunner = false; -} diff --git a/src/main/java/com/xclhove/xnote/config/SwaggerConfig.java b/src/main/java/com/xclhove/xnote/config/SwaggerConfig.java deleted file mode 100644 index fb40ec8..0000000 --- a/src/main/java/com/xclhove/xnote/config/SwaggerConfig.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.xclhove.xnote.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import springfox.documentation.builders.ApiInfoBuilder; -import springfox.documentation.builders.PathSelectors; -import springfox.documentation.builders.RequestHandlerSelectors; -import springfox.documentation.service.ApiInfo; -import springfox.documentation.spi.DocumentationType; -import springfox.documentation.spring.web.plugins.Docket; -import springfox.documentation.swagger2.annotations.EnableSwagger2; - - -/** - * swagger2配置类 - * - * @author xclhove - */ -@Configuration -@EnableSwagger2 -public class SwaggerConfig { - @Bean - public Docket api() { - return new Docket(DocumentationType.SWAGGER_2) - .select() - .apis(RequestHandlerSelectors.basePackage("com.xclhove.xnote.controller")) - .paths(PathSelectors.any()) - .build() - .apiInfo(apiInfo()); - } - - private ApiInfo apiInfo() { - return new ApiInfoBuilder() - .title("XNote API Documentation") - .description("This is XNote API Documentation.") - .termsOfServiceUrl("http://localhost:8080/") - .version("1.0.0") - .build(); - } -} - - diff --git a/src/main/java/com/xclhove/xnote/config/XnoteConfig.java b/src/main/java/com/xclhove/xnote/config/XnoteConfig.java new file mode 100644 index 0000000..e5dbc38 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/config/XnoteConfig.java @@ -0,0 +1,117 @@ +package com.xclhove.xnote.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.context.annotation.Configuration; + +/** + * @author xclhove + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "xnote") +@EnableConfigurationProperties(XnoteConfig.class) +public class XnoteConfig { + @NestedConfigurationProperty + public Debug debug = new Debug(); + @NestedConfigurationProperty + public Image image = new Image(); + @NestedConfigurationProperty + public Runner runner = new Runner(); + @NestedConfigurationProperty + public Search search = new Search(); + @NestedConfigurationProperty + public Interceptor interceptor = new Interceptor(); + + @Data + public static class Debug { + /** + * 是否开启验证码验证 + */ + private Boolean enableVerifyVerificationCode = true; + /** + * 是否开启模拟发送验证码 + */ + private Boolean enableMockSendVerificationCode = false; + } + + @Data + public static class Image { + /** + * 允许上传的图片大小(B),默认10M + */ + private Long allowSizeOfByte = 10485760L; + } + + @Data + public static class Runner { + /** + * 是否需要发送启动邮件(检查发送邮件功能是否正常) + */ + private Boolean sendStartupEmail = false; + /** + * 是否需要检查Minio服务状态 + */ + private Boolean enableCheckMinioStatus = true; + /** + * 是否需要检查Redis服务状态 + */ + private Boolean enableCheckRedisStatus = true; + /** + * 是否需要在启动时导入数据到ElasticSearch + */ + private Boolean importDataFromDatabaseToElasticSearch = false; + /** + * 启动时更新图片大小 + */ + private Boolean updateImageSize = false; + /** + * 启动时为用户创建默认笔记类型 + */ + private Boolean createDefaultNoteTypeForUserOfNoneType = false; + /** + * 启动时替换笔记中的图片 url + */ + private Boolean replaceImageUrlInNote = false; + } + + @Data + public static class Search { + /** + * 搜索结果最大条数 + */ + private int heightLightContentMaxLength = 200; + } + + @Data + public static class Interceptor { + public Ip ip = new Ip(); + public Device device = new Device(); + + @Data + public static class Ip { + /** + * 是否禁用Ip拦截器 + */ + private Boolean disable = false; + /** + * 单个ip每分钟最大请求次数 + */ + private int maxFrequencyPerMinute = 120; + } + + @Data + public static class Device { + /** + * 是否禁用Device拦截器 + */ + private Boolean disable = false; + /** + * 单个设备每分钟最大请求次数 + */ + private int maxFrequencyPerMinute = 120; + } + } +} diff --git a/src/main/java/com/xclhove/xnote/constant/EsConstant.java b/src/main/java/com/xclhove/xnote/constant/EsConstant.java new file mode 100644 index 0000000..43b68df --- /dev/null +++ b/src/main/java/com/xclhove/xnote/constant/EsConstant.java @@ -0,0 +1,14 @@ +package com.xclhove.xnote.constant; + +/** + * @author xclhove + */ +public interface EsConstant { + String NOTE_QUERY_FIELD = "queryField"; + + interface Analyzer { + String IK_SMART = "ik_smart"; + String IK_MAX_WORD = "ik_max_word"; + String STANDARD = "standard"; + } +} diff --git a/src/main/java/com/xclhove/xnote/constant/RedisKey.java b/src/main/java/com/xclhove/xnote/constant/RedisKey.java index b02966d..06e61d0 100644 --- a/src/main/java/com/xclhove/xnote/constant/RedisKey.java +++ b/src/main/java/com/xclhove/xnote/constant/RedisKey.java @@ -1,8 +1,76 @@ package com.xclhove.xnote.constant; +/** + * @author xclhove + */ public interface RedisKey { - public static final String VERIFICATION_CODE_OF = "verificationCodeOf"; - public static final String VERIFICATION_CODE_IP_LIMIT = "verificationCodeIpLimit"; - public static final String USER_TOKEN = "userToken"; - public static final String IP_FREQUENCY = "ipFrequency"; + String REDIS_CONNECT_TEST = "redisConnectTest"; + String PROJECT = "xnote"; + String USER = "user"; + String NOTE = "note"; + String INTERCEPTOR = "interceptor"; + String VERIFICATION_CODE = "verificationCode"; + String NOTE_TYPE = "noteType"; + String IMAGE = "image"; + String USER_IMAGE = "userImage"; + String SHARE_NOTE = "shareNote"; + String EXCEPTION = "exception"; + + static String join(String... keys) { + return String.join(":", keys); + } + + interface Interceptor { + String PREFIX = join(PROJECT, INTERCEPTOR); + String DEVICE = join(PREFIX, "device"); + String IP = join(PREFIX, "ip"); + } + + interface User { + String PREFIX = join(PROJECT, USER); + String TOKEN = join(PREFIX, "token"); + String ID = join(PREFIX, "id"); + String ACCOUNT = join(PREFIX, "account"); + String EMAIL = join(PREFIX, "email"); + } + + interface Note { + String PREFIX = join(PROJECT, NOTE); + } + + interface VerificationCode { + String PREFIX = join(PROJECT, VERIFICATION_CODE); + String EMAIL = join(PREFIX, "email"); + String IP = join(PREFIX, "ip"); + } + + interface NoteType { + String PREFIX = join(PROJECT, NOTE_TYPE); + String USER_ALL = join(PREFIX, "userAll"); + String ID = join(PREFIX, "id"); + } + + interface Image { + String PREFIX = join(PROJECT, IMAGE); + String ID = join(PREFIX, "id"); + String NAME = join(PREFIX, "name"); + String URL = join(PREFIX, "url"); + String USER_TOTAL_SIZE = join(PREFIX, "userTotalSize"); + } + + interface UserImage { + String PREFIX = join(PROJECT, USER_IMAGE); + String USER_AND_IMAGE_ID = join(PREFIX, "userAndImageId"); + } + + interface ShareNote { + String PREFIX = join(PROJECT, SHARE_NOTE); + String ID = join(PREFIX, "id"); + String CODE = join(PREFIX, "code"); + } + + interface Exception { + String PREFIX = join(PROJECT, EXCEPTION); + String CAN_SEND = join(PREFIX, "canSend"); + } } \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/constant/RequestHeaderKey.java b/src/main/java/com/xclhove/xnote/constant/RequestHeaderKey.java index 7ec73ef..dd83dad 100644 --- a/src/main/java/com/xclhove/xnote/constant/RequestHeaderKey.java +++ b/src/main/java/com/xclhove/xnote/constant/RequestHeaderKey.java @@ -4,6 +4,6 @@ * @author xclhove */ public interface RequestHeaderKey { - String TOKEN = "token"; + String AUTHORIZATION = "Authorization"; String USER_AGENT = "User-Agent"; } \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/constant/TreadLocalKey.java b/src/main/java/com/xclhove/xnote/constant/TreadLocalKey.java index a2fc71c..e4b9776 100644 --- a/src/main/java/com/xclhove/xnote/constant/TreadLocalKey.java +++ b/src/main/java/com/xclhove/xnote/constant/TreadLocalKey.java @@ -4,7 +4,7 @@ * @author xclhove */ public interface TreadLocalKey { - String ID = "id"; String DEVICE_ID = "deviceId"; String CLIENT_IP = "clientIp"; + String USER = "user"; } \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/controller/ImageController.java b/src/main/java/com/xclhove/xnote/controller/ImageController.java index f72460e..58c0d99 100644 --- a/src/main/java/com/xclhove/xnote/controller/ImageController.java +++ b/src/main/java/com/xclhove/xnote/controller/ImageController.java @@ -1,20 +1,27 @@ package com.xclhove.xnote.controller; -import com.xclhove.xnote.Interceptor.UserJwtInterceptor; -import com.xclhove.xnote.constant.TreadLocalKey; -import com.xclhove.xnote.entity.dto.ImagePageDTO; -import com.xclhove.xnote.entity.table.Image; + +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.xclhove.xnote.exception.ImageServiceException; +import com.xclhove.xnote.interceptor.UserTokenInterceptor; +import com.xclhove.xnote.pojo.table.User; +import com.xclhove.xnote.pojo.vo.PageVO; +import com.xclhove.xnote.pojo.vo.SearchUserImageVO; import com.xclhove.xnote.service.ImageService; -import com.xclhove.xnote.util.Result; -import com.xclhove.xnote.util.ThreadLocalUtil; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; +import com.xclhove.xnote.service.UserImageService; +import com.xclhove.xnote.tool.Result; +import com.xclhove.xnote.tool.ThreadLocalTool; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletResponse; -import javax.validation.constraints.Pattern; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.io.IOException; import java.util.List; /** @@ -24,74 +31,67 @@ */ @RestController @RequestMapping("/image") -@Api(tags = "图片相关接口") @RequiredArgsConstructor +@Validated +@Slf4j public class ImageController { private final ImageService imageService; + private final UserImageService userImageService; - @GetMapping("downloadByName/{imageName}") - @ApiOperation(value = "下载图片") - public void downloadByName(HttpServletResponse response, @PathVariable String imageName) { - imageService.downloadByName(response, imageName); - } - - @GetMapping("downloadById/{imageId}") - @ApiOperation(value = "下载图片") - @UserJwtInterceptor.UserJwtIntercept() - public void downloadById(HttpServletResponse response, - @PathVariable - @Pattern(regexp = "^\\d+$", message = "图片id必须为数字") - Integer imageId) { - Integer userId = (Integer) ThreadLocalUtil.get(TreadLocalKey.ID); - imageService.downloadById(response, userId, imageId); + /** + * 上传图片 + */ + @PostMapping + @UserTokenInterceptor.UserTokenIntercept() + public Result upload(@NotNull(message = "图片不能为空") MultipartFile uploadImageFile) { + User user = ThreadLocalTool.getUser(); + + String imageName = imageService.upload(user, uploadImageFile); + return Result.success(imageName); } - @PutMapping - @ApiOperation(value = "上传图片") - @UserJwtInterceptor.UserJwtIntercept - public Result upload(MultipartFile uploadImage) { - Integer id = (Integer) ThreadLocalUtil.get("id"); - Image image = imageService.upload(id, uploadImage); - return Result.success(image); + /** + * 预览图片 + */ + @GetMapping("/name/{imageName}") + public void previewImage(HttpServletResponse response, @PathVariable String imageName) { + String imageUrl = imageService.getImageUrlByNameWithRedis(imageName); + if (imageUrl == null) { + throw new ImageServiceException("图片不存在"); + } + try { + response.setStatus(302); + response.sendRedirect(imageUrl); + } catch (IOException e) { + log.error("重定向失败", e); + throw new ImageServiceException("重定向失败"); + } } - @PostMapping("/deleteByIds") - @ApiOperation(value = "通过id删除图片") - @UserJwtInterceptor.UserJwtIntercept - public Result deleteByIds(@RequestBody List ids) { - Integer userId = (Integer) ThreadLocalUtil.get(TreadLocalKey.ID); - imageService.deleteByIds(userId, ids); - return Result.success(); + /** + * 搜索自己的图片 + */ + @GetMapping("me") + @UserTokenInterceptor.UserTokenIntercept() + public Result> searchSelfImage( + @RequestParam(defaultValue = "1", required = false) Integer page, + @RequestParam(defaultValue = "10", required = false) Integer size, + @RequestParam(required = false) String search + ) { + User user = ThreadLocalTool.getUser(); + Page pageResult = userImageService.searchUserImage(new Page<>(page, size), user, search); + PageVO pageVO = BeanUtil.copyProperties(pageResult, PageVO.class); + return Result.success(pageVO); } - @PostMapping - @ApiOperation(value = "修改图片") - @UserJwtInterceptor.UserJwtIntercept - public Result change(@RequestBody Image image) { - Integer userId = (Integer) ThreadLocalUtil.get("id"); - image.setUserId(userId); - imageService.change(image); + /** + * 删除图片 + */ + @DeleteMapping + @UserTokenInterceptor.UserTokenIntercept + public Result delete(@NotEmpty(message = "用户图片ID不能为空") @RequestParam List userImageIds) { + User user = ThreadLocalTool.getUser(); + userImageService.removeUserImageByIds(user, userImageIds); return Result.success(); } - - @GetMapping("/{imageId}") - @ApiOperation(value = "获取图片信息") - @UserJwtInterceptor.UserJwtIntercept - public Result get(@PathVariable - @Pattern(regexp = "^\\d+$", message = "图片id必须为数字") - Integer imageId) { - Integer userId = (Integer) ThreadLocalUtil.get("id"); - Image image = imageService.get(userId, imageId); - return Result.success(image); - } - - @PostMapping("/page") - @ApiOperation(value = "分页获取图片") - @UserJwtInterceptor.UserJwtIntercept - public Result page(@RequestBody ImagePageDTO pageDTO) { - Integer userId = (Integer) ThreadLocalUtil.get("id"); - pageDTO.setUserId(userId); - ImagePageDTO imagePageDTO = imageService.page(pageDTO); - return Result.success(imagePageDTO); - } -} \ No newline at end of file +} diff --git a/src/main/java/com/xclhove/xnote/controller/NoteController.java b/src/main/java/com/xclhove/xnote/controller/NoteController.java index fef160e..bb198e6 100644 --- a/src/main/java/com/xclhove/xnote/controller/NoteController.java +++ b/src/main/java/com/xclhove/xnote/controller/NoteController.java @@ -1,20 +1,26 @@ package com.xclhove.xnote.controller; -import com.xclhove.xnote.Interceptor.UserJwtInterceptor; -import com.xclhove.xnote.Interceptor.validator.UserJwtValidator; -import com.xclhove.xnote.constant.TreadLocalKey; -import com.xclhove.xnote.entity.dto.NotePageDTO; -import com.xclhove.xnote.entity.table.Note; + +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.xclhove.xnote.exception.ParameterValidateException; +import com.xclhove.xnote.interceptor.UserTokenInterceptor; +import com.xclhove.xnote.pojo.form.note.NoteAddForm; +import com.xclhove.xnote.pojo.form.note.NoteUpdateForm; +import com.xclhove.xnote.pojo.form.note.NoteUpdateTypeForm; +import com.xclhove.xnote.pojo.table.Note; +import com.xclhove.xnote.pojo.table.User; +import com.xclhove.xnote.pojo.vo.PageVO; +import com.xclhove.xnote.pojo.vo.SearchNoteVO; import com.xclhove.xnote.service.NoteService; -import com.xclhove.xnote.util.Result; -import com.xclhove.xnote.util.ThreadLocalUtil; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; +import com.xclhove.xnote.tool.Result; +import com.xclhove.xnote.tool.ThreadLocalTool; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import javax.validation.constraints.Pattern; +import java.util.List; /** * 笔记相关接口 @@ -22,75 +28,108 @@ * @author xclhove */ @RestController -@RequestMapping("/notes") -@Api(tags = "笔记相关接口") +@RequestMapping("/note") @RequiredArgsConstructor +@Slf4j +@Validated public class NoteController { private final NoteService noteService; - @PostMapping("/page") - @ApiOperation(value = "分页获取笔记(不包含内容)") - @UserJwtValidator.UserJwtValidate - public Result pageAllNote(@RequestBody NotePageDTO notePageDTO) { - Integer userId = (Integer) ThreadLocalUtil.get(TreadLocalKey.ID); - notePageDTO = noteService.pageNote(notePageDTO, userId); - return Result.success(notePageDTO); + private void checkSearchTextLength(String searchText) throws ParameterValidateException { + if (searchText != null && searchText.length() > 500) { + throw new ParameterValidateException("搜索内容过长"); + } } - @PostMapping("/page/me") - @ApiOperation(value = "分页获取自己的笔记(不包含内容)") - @UserJwtInterceptor.UserJwtIntercept - public Result pageUserNote(@RequestBody NotePageDTO notePageDTO) { - Integer userId = (Integer) ThreadLocalUtil.get(TreadLocalKey.ID); - notePageDTO.setUserId(userId); - notePageDTO = noteService.pageNote(notePageDTO, userId, true); - return Result.success(notePageDTO); + /** + * 增加笔记 + */ + @PostMapping + @UserTokenInterceptor.UserTokenIntercept + public Result addNote(@RequestBody @Validated NoteAddForm noteAddForm) { + User user = ThreadLocalTool.getUser(); + int noteId = noteService.add(user, noteAddForm); + return Result.success(noteId); } - @GetMapping("/{noteId}") - @ApiOperation(value = "获取一篇笔记") - @UserJwtValidator.UserJwtValidate - public Result getOneNote( - @PathVariable - @ApiParam(value = "笔记id", example = "1") - @Pattern(regexp = "^\\d+$", message = "笔记id只能是数字!") - Integer noteId, - - @RequestParam(required = false) - @ApiParam(value = "访问码", example = "123456") - @Pattern(regexp = "^[a-zA-z1-9]*$", message = "访问码只支持数字和字母!") - String accessCode) { - Integer userId = (Integer) ThreadLocalUtil.get(TreadLocalKey.ID); - Note note = noteService.getNoteById(userId, noteId, accessCode); - return Result.success(note); + /** + * 批量删除笔记 + */ + @DeleteMapping + @UserTokenInterceptor.UserTokenIntercept + public Result deleteNoteBatchByIds(@RequestParam List noteIds) { + User user = ThreadLocalTool.getUser(); + noteService.deleteBatchByIds(user, noteIds); + return Result.success(); } + /** + * 更新笔记 + */ @PutMapping - @UserJwtInterceptor.UserJwtIntercept - @ApiOperation(value = "添加笔记") - public Result addNote(@RequestBody @ApiParam(value = "笔记信息") Note note) { - Integer userId = (Integer) ThreadLocalUtil.get("id"); - note.setUserId(userId); - note = noteService.addNote(note); - return Result.success(note); + @UserTokenInterceptor.UserTokenIntercept + public Result updateNote(@RequestBody @Validated NoteUpdateForm noteUpdateForm) { + User user = ThreadLocalTool.getUser(); + noteService.update(user, noteUpdateForm); + return Result.success(); } - @DeleteMapping("/{noteId}") - @UserJwtInterceptor.UserJwtIntercept - @ApiOperation(value = "删除笔记") - public Result deleteNote(@PathVariable @ApiParam(value = "笔记id", example = "1") Integer noteId) { - Integer userId = (Integer) ThreadLocalUtil.get("id"); - noteService.deleteNote(userId, noteId); + /** + * 批量更新笔记类型 + */ + @PutMapping("types") + @UserTokenInterceptor.UserTokenIntercept + public Result updateBatchType(@RequestBody @Validated NoteUpdateTypeForm noteUpdateTypeForm) { + User user = ThreadLocalTool.getUser(); + noteService.updateBatchUserNoteType(user, noteUpdateTypeForm); return Result.success(); } - @PostMapping - @UserJwtInterceptor.UserJwtIntercept - @ApiOperation(value = "更新笔记") - public Result updateNote(@RequestBody @ApiParam(value = "笔记信息") Note note) { - Integer userId = (Integer) ThreadLocalUtil.get("id"); - note.setUserId(userId); - note = noteService.updateNote(note); + /** + * 查看笔记 + */ + @GetMapping("{noteId}") + @UserTokenInterceptor.UserTokenIntercept(needIntercept = false) + public Result previewNote(@PathVariable Integer noteId, @RequestParam(required = false) String shareCode) { + User user = ThreadLocalTool.getUser(); + Note note = noteService.previewNote(user, noteId, shareCode); return Result.success(note); } + + /** + * 搜索笔记 + */ + @GetMapping("search") + @UserTokenInterceptor.UserTokenIntercept(needIntercept = false) + public Result> searchNote( + @RequestParam(required = false, defaultValue = "1") Integer current, + @RequestParam(required = false, defaultValue = "10") Integer size, + @RequestParam(required = false, defaultValue = "") String search, + @RequestParam(required = false, defaultValue = "") String heightLightPreTag, + @RequestParam(required = false, defaultValue = "") String heightLightPostTag + ) { + checkSearchTextLength(search); + User user = ThreadLocalTool.getUser(); + PageVO pageVO = noteService.search(new Page<>(current, size), user, search, heightLightPreTag, heightLightPostTag); + return Result.success(pageVO); + } + + /** + * 搜索用户笔记 + */ + @GetMapping("search/{userId}") + @UserTokenInterceptor.UserTokenIntercept(needIntercept = false) + public Result> searchUserNote( + @PathVariable Integer userId, + @RequestParam(required = false, defaultValue = "1") Integer current, + @RequestParam(required = false, defaultValue = "10") Integer size, + @RequestParam(required = false, defaultValue = "") String search, + @RequestParam(required = false) Integer typeId + ) { + checkSearchTextLength(search); + User user = ThreadLocalTool.getUser(); + Page pageResult = noteService.searchUserNote(new Page<>(current, size), user, userId, typeId, search); + PageVO pageVO = BeanUtil.copyProperties(pageResult, PageVO.class); + return Result.success(pageVO); + } } diff --git a/src/main/java/com/xclhove/xnote/controller/NoteTypeController.java b/src/main/java/com/xclhove/xnote/controller/NoteTypeController.java new file mode 100644 index 0000000..f2c2475 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/controller/NoteTypeController.java @@ -0,0 +1,70 @@ +package com.xclhove.xnote.controller; + + +import com.xclhove.xnote.interceptor.UserTokenInterceptor; +import com.xclhove.xnote.pojo.form.noteType.NoteTypeAddForm; +import com.xclhove.xnote.pojo.form.noteType.NoteTypeUpdateForm; +import com.xclhove.xnote.pojo.table.NoteType; +import com.xclhove.xnote.pojo.table.User; +import com.xclhove.xnote.service.NoteTypeService; +import com.xclhove.xnote.tool.Result; +import com.xclhove.xnote.tool.ThreadLocalTool; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 笔记分类相关接口 + * + * @author xclhove + */ +@RestController +@RequestMapping("/note-type") +@RequiredArgsConstructor +public class NoteTypeController { + private final NoteTypeService noteTypeService; + + /** + * 添加笔记分类 + */ + @PostMapping + @UserTokenInterceptor.UserTokenIntercept + public Result addNoteType(@RequestBody @Validated NoteTypeAddForm noteTypeAddForm) { + User user = ThreadLocalTool.getUser(); + noteTypeService.addNoteType(user, noteTypeAddForm); + return Result.success(); + } + + /** + * 获取用户所有笔记分类 + */ + @GetMapping("/user/{userId}") + public Result> getUserAllNoteType(@PathVariable Integer userId) { + List list = noteTypeService.getUserAllNoteTypeWithRedis(userId); + return Result.success(list); + } + + /** + * 删除笔记分类 + */ + @DeleteMapping + @UserTokenInterceptor.UserTokenIntercept + public Result deleteNoteType(@RequestParam List ids) { + User user = ThreadLocalTool.getUser(); + noteTypeService.deleteNoteTypeByIds(user, ids); + return Result.success(); + } + + /** + * 修改笔记分类 + */ + @PutMapping + @UserTokenInterceptor.UserTokenIntercept + public Result updateNoteType(@RequestBody @Validated NoteTypeUpdateForm noteTypeUpdateForm) { + User user = ThreadLocalTool.getUser(); + noteTypeService.updateNoteType(user, noteTypeUpdateForm); + return Result.success(); + } +} diff --git a/src/main/java/com/xclhove/xnote/controller/RequestErrorController.java b/src/main/java/com/xclhove/xnote/controller/RequestErrorController.java index 4233c0e..94b7ac5 100644 --- a/src/main/java/com/xclhove/xnote/controller/RequestErrorController.java +++ b/src/main/java/com/xclhove/xnote/controller/RequestErrorController.java @@ -1,26 +1,31 @@ package com.xclhove.xnote.controller; import com.xclhove.xnote.exception.NotFoundException; -import io.swagger.annotations.Api; +import com.xclhove.xnote.tool.Result; +import lombok.extern.slf4j.Slf4j; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; +import javax.servlet.http.HttpServletRequest; + /** * 请求错误相关接口 * * @author xclhove */ @Controller -@Api(tags = "请求错误相关接口") -@Order() +@Slf4j +@Order public class RequestErrorController { /** * 请求路径不存在 */ @RequestMapping(value = "/**") - public String notFound() { - throw new NotFoundException(); + public Result notFound(HttpServletRequest request) { + String message = String.format("请求路径不存在:%s", request.getRequestURI()); + log.error(message); + throw new NotFoundException(message); } } diff --git a/src/main/java/com/xclhove/xnote/controller/ShareNoteRecordController.java b/src/main/java/com/xclhove/xnote/controller/ShareNoteRecordController.java new file mode 100644 index 0000000..c34ac62 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/controller/ShareNoteRecordController.java @@ -0,0 +1,79 @@ +package com.xclhove.xnote.controller; + + +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.xclhove.xnote.interceptor.UserTokenInterceptor; +import com.xclhove.xnote.pojo.form.shareNoteRecord.ShareNoteRecordCreateForm; +import com.xclhove.xnote.pojo.form.shareNoteRecord.ShareNoteRecordUpdateForm; +import com.xclhove.xnote.pojo.table.User; +import com.xclhove.xnote.pojo.vo.PageVO; +import com.xclhove.xnote.pojo.vo.ShareNoteRecordVO; +import com.xclhove.xnote.service.ShareNoteRecordService; +import com.xclhove.xnote.tool.Result; +import com.xclhove.xnote.tool.ThreadLocalTool; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 笔记分享记录相关接口 + * + * @author xclhove + */ +@RestController +@RequestMapping("/share-note-record") +@RequiredArgsConstructor +public class ShareNoteRecordController { + private final ShareNoteRecordService shareNoteRecordService; + + /** + * 分享笔记 + * @return 分享码 + */ + @PostMapping + @UserTokenInterceptor.UserTokenIntercept + public Result shareNote(@RequestBody @Validated ShareNoteRecordCreateForm shareNoteRecordCreateForm) { + User user = ThreadLocalTool.getUser(); + String code = shareNoteRecordService.share(user, shareNoteRecordCreateForm); + return Result.success(code); + } + + /** + * 批量删除分享的记录 + */ + @DeleteMapping + @UserTokenInterceptor.UserTokenIntercept + public Result deleteShareNoteByIds(@RequestParam List shareNoteRecordIds) { + User user = ThreadLocalTool.getUser(); + shareNoteRecordService.deleteShareNotes(user, shareNoteRecordIds); + return Result.success(); + } + + /** + * 获取分享的记录 + */ + @GetMapping("me") + @UserTokenInterceptor.UserTokenIntercept + public Result> searchShareNote( + @RequestParam(required = false, defaultValue = "1") Integer current, + @RequestParam(required = false, defaultValue = "10") Integer size + ) { + User user = ThreadLocalTool.getUser(); + Page pageResult = shareNoteRecordService.getShareNoteList(user, new Page<>(current, size)); + PageVO pageVO = BeanUtil.copyProperties(pageResult, PageVO.class); + return Result.success(pageVO); + } + + /** + * 修改笔记分享记录 + */ + @PutMapping + public Result updateShareNoteRecord(@RequestBody @Validated ShareNoteRecordUpdateForm shareNoteRecordCreateForm) { + User user = ThreadLocalTool.getUser(); + shareNoteRecordService.update(user, shareNoteRecordCreateForm); + return Result.success(); + } +} diff --git a/src/main/java/com/xclhove/xnote/controller/SwaggerController.java b/src/main/java/com/xclhove/xnote/controller/SwaggerController.java deleted file mode 100644 index 4141ab4..0000000 --- a/src/main/java/com/xclhove/xnote/controller/SwaggerController.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.xclhove.xnote.controller; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * swagger - * - * @author xclhove - */ -@Controller -@RequestMapping("/swagger") -@Slf4j -public class SwaggerController { - - @GetMapping() - public void swaggerDocs(HttpServletRequest request, HttpServletResponse response) { - String url = "/swagger-ui.html"; - try { - response.sendRedirect(url); - } catch (Exception e) { - log.error(e.getMessage()); - } - } - - @GetMapping("/apis") - public void swaggerApis(HttpServletRequest request, HttpServletResponse response) { - String url = "/v2/api-docs"; - try { - request.getRequestDispatcher(url).forward(request, response); - } catch (Exception e) { - log.error(e.getMessage()); - } - } - -} diff --git a/src/main/java/com/xclhove/xnote/controller/UserController.java b/src/main/java/com/xclhove/xnote/controller/UserController.java index 18c2087..91f1ee9 100644 --- a/src/main/java/com/xclhove/xnote/controller/UserController.java +++ b/src/main/java/com/xclhove/xnote/controller/UserController.java @@ -1,147 +1,142 @@ package com.xclhove.xnote.controller; + import cn.hutool.core.bean.BeanUtil; -import com.xclhove.xnote.Interceptor.AdminJwtInterceptor; -import com.xclhove.xnote.Interceptor.UserJwtInterceptor; -import com.xclhove.xnote.Interceptor.validator.UserJwtValidator; -import com.xclhove.xnote.constant.RedisKey; -import com.xclhove.xnote.constant.RequestHeaderKey; -import com.xclhove.xnote.constant.TreadLocalKey; -import com.xclhove.xnote.entity.dto.UserDTO; -import com.xclhove.xnote.entity.table.User; -import com.xclhove.xnote.exception.OperationFrequencyException; -import com.xclhove.xnote.exception.VerificationCodeException; +import com.xclhove.xnote.exception.UserServiceException; +import com.xclhove.xnote.interceptor.UserTokenInterceptor; +import com.xclhove.xnote.pojo.form.user.*; +import com.xclhove.xnote.pojo.table.User; import com.xclhove.xnote.service.UserService; -import com.xclhove.xnote.tool.RedisTool; -import com.xclhove.xnote.util.Result; -import com.xclhove.xnote.util.ThreadLocalUtil; -import com.xclhove.xnote.util.TokenUtil; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; +import com.xclhove.xnote.service.VerificationCodeService; +import com.xclhove.xnote.tool.Result; +import com.xclhove.xnote.tool.ThreadLocalTool; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import javax.servlet.http.HttpServletRequest; -import javax.validation.constraints.Pattern; -import java.util.concurrent.TimeUnit; - /** + * 用户相关接口 + * * @author xclhove */ @RestController -@RequestMapping("/users") -@Api(tags = "用户相关接口") +@RequestMapping("/user") @RequiredArgsConstructor @Validated public class UserController { private final UserService userService; - private final RedisTool redisTool; + private final VerificationCodeService verificationCodeService; - @GetMapping("/{userId}") - @ApiOperation(value = "查询用户信息") - public Result queryUserById(@PathVariable - @ApiParam(value = "用户id", example = "1") - @Pattern(regexp = "^\\d+$", message = "用户id必须为数字!") - Integer userId) { - User user = userService.queryById(userId); - return Result.success(user); + /** + * 注册 + */ + @PostMapping("/register") + public Result register(@Validated @RequestBody UserRegisterForm userRegisterForm) { + boolean verified = verificationCodeService.verify(userRegisterForm.getVerificationCode(), userRegisterForm.getEmail()); + if (!verified) { + throw new UserServiceException("验证码错误!"); + } + + User user = BeanUtil.copyProperties(userRegisterForm, User.class); + userService.register(user); + return Result.success(); } - @GetMapping("/login") - @ApiOperation(value = "用户登录") - public Result login(@RequestParam - @ApiParam(value = "账号", example = "user123456") - @Pattern(regexp = "^[a-zA-Z0-9_.*]{5,30}$", message = "账号仅支持5到30位的数字、字母、‘_’、‘.’和‘*’!") - String account, - @RequestParam - @ApiParam(value = "密码", example = "123456") - @Pattern(regexp = "^[a-zA-Z0-9_.*]{5,30}$", message = "账号仅支持5到30位的数字、字母、‘_’、‘.’和‘*’!") - String password) { - String token = userService.login(account, password); - return Result.success("登录成功!", token); + /** + * 登录 + */ + @PostMapping("/login") + public Result login(@Validated @RequestBody UserLoginForm userLoginForm) { + User user = BeanUtil.copyProperties(userLoginForm, User.class); + String deviceId = ThreadLocalTool.getDeviceId(); + + String token = userService.login(user, deviceId); + return Result.success(token); } + /** + * 注销 + */ + @UserTokenInterceptor.UserTokenIntercept @PostMapping("/logout") - @UserJwtValidator.UserJwtValidate - @ApiOperation(value = "用户登出") - public Result logout(@RequestHeader(value = RequestHeaderKey.TOKEN, defaultValue = "") String token) { - Integer userId = TokenUtil.getId(token); - if (userId != null) { - userService.logout(userId); + public Result logout() { + String deviceId = ThreadLocalTool.getDeviceId(); + User user = ThreadLocalTool.getUser(); + if (user == null) { + return Result.success(); } - return Result.success("登出成功!", null); + + userService.logout(user, deviceId); + return Result.success(); } - @PostMapping("/login") - @ApiOperation(value = "用户登录") - public Result login(@RequestBody UserDTO userDTO) { - return this.login(userDTO.getAccount(), userDTO.getPassword()); + /** + * 根据id获取用户信息 + */ + @UserTokenInterceptor.UserTokenIntercept + @GetMapping("{userId}") + public Result getById(@PathVariable Integer userId) { + User user = userService.getByIdWithRedis(userId); + return Result.success(user); } - @GetMapping("/self") - @UserJwtInterceptor.UserJwtIntercept - @ApiOperation(value = "查询用户自己的信息") - public Result querySelfInfo() { - Integer userId = ThreadLocalUtil.get(TreadLocalKey.ID, Integer.class); - User user = userService.queryById(userId); - return Result.success(user); + /** + * 获取自己的用户信息 + */ + @UserTokenInterceptor.UserTokenIntercept + @GetMapping("me") + public Result getSelfInfo() { + return Result.success(ThreadLocalTool.getUser()); } - @PutMapping("/register") - @ApiOperation(value = "用户注册") - public Result register(@RequestBody - @ApiParam(value = "用户信息") - UserDTO userDTO) { - boolean verifiedPassed = userService.verifyVerificationCode(userDTO.getEmail(), userDTO.getVerificationCode()); - if (!verifiedPassed) throw new VerificationCodeException("验证码错误!"); - User user = BeanUtil.copyProperties(userDTO, User.class); - userService.register(user); - return Result.success("注册成功!", null); + /** + * 刷新token + */ + @UserTokenInterceptor.UserTokenIntercept + @GetMapping("new-token") + public Result getNewToken() { + User user = ThreadLocalTool.getUser(); + String deviceId = ThreadLocalTool.getDeviceId(); + + String token = userService.login(user, deviceId); + return Result.success(token); } - @PostMapping - @UserJwtInterceptor.UserJwtIntercept - @ApiOperation(value = "更新用户信息") - public Result update(HttpServletRequest request, - @RequestBody - @ApiParam(value = "用户信息") - UserDTO userDTO) { - String token = request.getHeader(RequestHeaderKey.TOKEN); - Integer userId = TokenUtil.getId(token); - User user = BeanUtil.copyProperties(userDTO, User.class); - user.setId(userId); - userService.updateInfo(user); + /** + * 更新用户信息 + */ + @PutMapping + @UserTokenInterceptor.UserTokenIntercept + public Result update(@Validated @RequestBody UserUpdateForm userUpdateForm) { + User user = ThreadLocalTool.getUser(); + userService.updateCommonInfo(user, userUpdateForm); return Result.success(); } - @PostMapping("/ban/{userId}") - @AdminJwtInterceptor.AdminJwtIntercept - @ApiOperation("禁封用户") - public Result ban(@PathVariable - @ApiParam(value = "用户id", example = "1") - Integer userId) { - userService.banById(userId); + /** + * 更新密码 + */ + @PutMapping("password") + @UserTokenInterceptor.UserTokenIntercept + public Result updatePassword(@Validated @RequestBody UserUpdatePasswordForm userUpdatePasswordForm) { + User user = ThreadLocalTool.getUser(); + userService.updatePassword(user, userUpdatePasswordForm); return Result.success(); } - @GetMapping("/verificationCode") - @ApiOperation("发送验证码") - public Result sendVerificationCode(HttpServletRequest request, - @RequestParam - @ApiParam(value = "邮箱") - String email) { - String ip = request.getRemoteAddr(); - String redisKey = RedisKey.VERIFICATION_CODE_IP_LIMIT + ip; - Integer value = redisTool.getValue(redisKey, Integer.class); - - final int maxFrequencyPerMinute = 2; - int frequency = (value == null) ? 0 : value; - if (frequency >= maxFrequencyPerMinute) throw new OperationFrequencyException(); + /** + * 更新邮箱 + */ + @PutMapping("email") + @UserTokenInterceptor.UserTokenIntercept + public Result updateEmail(@Validated @RequestBody UserUpdateEmailForm userUpdateEmailForm) { + boolean verified = verificationCodeService.verify(userUpdateEmailForm.getVerificationCode(), userUpdateEmailForm.getEmail()); + if (!verified) { + throw new UserServiceException("验证码错误!"); + } - userService.sendVerificationCode(email); - redisTool.setValue(redisKey, ++frequency, 1, TimeUnit.MINUTES); - return Result.success("验证码发送成功!", null); + User user = ThreadLocalTool.getUser(); + userService.updateEmail(user, userUpdateEmailForm); + return Result.success(); } } diff --git a/src/main/java/com/xclhove/xnote/controller/VerificationCodeController.java b/src/main/java/com/xclhove/xnote/controller/VerificationCodeController.java new file mode 100644 index 0000000..718e51a --- /dev/null +++ b/src/main/java/com/xclhove/xnote/controller/VerificationCodeController.java @@ -0,0 +1,118 @@ +package com.xclhove.xnote.controller; + + +import cn.hutool.core.util.StrUtil; +import com.xclhove.xnote.constant.RedisKey; +import com.xclhove.xnote.exception.VerificationCodeException; +import com.xclhove.xnote.interceptor.IpInterceptor; +import com.xclhove.xnote.service.VerificationCodeService; +import com.xclhove.xnote.tool.Result; +import com.xclhove.xnote.tool.ThreadLocalTool; +import com.xclhove.xnote.util.ValidateCodeImageUtil; +import com.xclhove.xnote.util.VerificationCodeUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.imageio.ImageIO; +import javax.servlet.http.HttpServletResponse; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * 验证码相关接口 + * + * @author xclhove + */ +@RestController +@RequestMapping("/verification-code") +@RequiredArgsConstructor +@Validated +@Slf4j +public class VerificationCodeController { + private final VerificationCodeService verificationCodeService; + private final StringRedisTemplate stringRedisTemplate; + + public String getRedisKeyByIpAddress(String ipAddress) { + return RedisKey.join(RedisKey.VerificationCode.IP, ipAddress); + } + + public String generateImageCode() { + String simpleCode = VerificationCodeUtil.generate(4, true); + stringRedisTemplate.opsForValue().set( + getRedisKeyByIpAddress(ThreadLocalTool.getClientIpAddress()), + simpleCode, + 60, + TimeUnit.SECONDS + ); + return simpleCode; + } + + /** + * 发送验证码到邮箱 + */ + @GetMapping("/send/to-email") + @IpInterceptor.PathFrequencyLimit(maxFrequencyPerMinute = 3, message = "发送验证码过于频繁") + public Result sendVerificationCodeToEmail( + @NotBlank(message = "邮箱不能为空") + @Email(message = "邮箱格式不正确") + @RequestParam + String email, + @NotBlank(message = "图片验证码不能为空") + @RequestParam + String imageCode + ) { + String redisKey = getRedisKeyByIpAddress(ThreadLocalTool.getClientIpAddress()); + String codeInRedis = stringRedisTemplate.opsForValue().get(redisKey); + if (StrUtil.isBlank(codeInRedis)) { + throw new VerificationCodeException("验证码已过期,请重新获取"); + } + if (!codeInRedis.equals(imageCode)) { + throw new VerificationCodeException("图片验证码错误"); + } + stringRedisTemplate.delete(redisKey); + + int expireSeconds = verificationCodeService.sendToEmail(email); + return Result.success(expireSeconds); + } + + /** + * 获取图片验证码的base64 + */ + @GetMapping("/image/base64") + @IpInterceptor.PathFrequencyLimit(maxFrequencyPerMinute = 10, message = "获取图片验证码过于频繁") + public Result getVerificationCodeImageBase64() { + String simpleCode = generateImageCode(); + try { + String imageBase64 = ValidateCodeImageUtil.generateImageBase64(simpleCode); + return Result.success(imageBase64); + } catch (IOException e) { + throw new VerificationCodeException("获取图片验证码失败,请稍后再试"); + } + } + + /** + * 获取图片验证码 + */ + @GetMapping("/image") + @IpInterceptor.PathFrequencyLimit(maxFrequencyPerMinute = 10, message = "获取图片验证码过于频繁") + public void getVerificationCodeImage(HttpServletResponse response) { + String simpleCode = generateImageCode(); + try { + BufferedImage bufferedImage = ValidateCodeImageUtil.generateImage(simpleCode); + String contentType = ValidateCodeImageUtil.CONTENT_TYPE; + response.setContentType(contentType); + ImageIO.write(bufferedImage, contentType.substring(contentType.indexOf("/") + 1), response.getOutputStream()); + } catch (IOException e) { + throw new VerificationCodeException("获取图片验证码失败,请稍后再试"); + } + } +} diff --git a/src/main/java/com/xclhove/xnote/entity/attribute/NoteKeyword.java b/src/main/java/com/xclhove/xnote/entity/attribute/NoteKeyword.java deleted file mode 100644 index 45dd334..0000000 --- a/src/main/java/com/xclhove/xnote/entity/attribute/NoteKeyword.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.xclhove.xnote.entity.attribute; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 关键词 - * - * @author xclhove - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -public class NoteKeyword { - private Integer id; - private String name; -} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/entity/dto/ImagePageDTO.java b/src/main/java/com/xclhove/xnote/entity/dto/ImagePageDTO.java deleted file mode 100644 index 8b4822f..0000000 --- a/src/main/java/com/xclhove/xnote/entity/dto/ImagePageDTO.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.xclhove.xnote.entity.dto; - -import com.xclhove.xnote.entity.table.Image; -import io.swagger.annotations.ApiParam; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * Image分页数据传输对象 - * @author xclhove - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -public class ImagePageDTO extends PageDTO{ - @ApiParam(value = "搜索图片别名") - private String searchAlias; - - - @ApiParam(value = "用户id") - private Integer userId; -} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/entity/dto/NotePageDTO.java b/src/main/java/com/xclhove/xnote/entity/dto/NotePageDTO.java deleted file mode 100644 index 0c1651c..0000000 --- a/src/main/java/com/xclhove/xnote/entity/dto/NotePageDTO.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.xclhove.xnote.entity.dto; - -import com.xclhove.xnote.entity.table.Note; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiParam; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * Note分页数据传输对象 - * - * @author xclhove - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -@ApiModel(value = "Note分页数据传输对象") -public class NotePageDTO extends PageDTO { - - @ApiParam(value = "搜索标题") - private String searchTitle; - - - @ApiParam(value = "搜索内容") - private String searchContent; - - - @ApiParam(value = "搜索关键词") - private String searchKeyword; - - - @ApiParam(value = "用户id") - private Integer userId; -} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/entity/dto/ObjectList.java b/src/main/java/com/xclhove/xnote/entity/dto/ObjectList.java deleted file mode 100644 index 420f1d1..0000000 --- a/src/main/java/com/xclhove/xnote/entity/dto/ObjectList.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.xclhove.xnote.entity.dto; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -/** - * @author xclhove - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -public class ObjectList { - private List value; -} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/entity/dto/PageDTO.java b/src/main/java/com/xclhove/xnote/entity/dto/PageDTO.java deleted file mode 100644 index 59a1a37..0000000 --- a/src/main/java/com/xclhove/xnote/entity/dto/PageDTO.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.xclhove.xnote.entity.dto; - -import io.swagger.annotations.ApiParam; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -/** - * 分页数据传输对象 - * - * @author xclhove - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -public class PageDTO { - - @ApiParam(value = "页码") - private Integer current; - - @ApiParam(value = "页大小") - private Integer size; - - @ApiParam(value = "总数") - private Integer total; - - @ApiParam(value = "数据列表") - private List list; -} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/entity/dto/UserDTO.java b/src/main/java/com/xclhove/xnote/entity/dto/UserDTO.java deleted file mode 100644 index da12ba3..0000000 --- a/src/main/java/com/xclhove/xnote/entity/dto/UserDTO.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.xclhove.xnote.entity.dto; - -import com.baomidou.mybatisplus.annotation.TableId; -import com.xclhove.xnote.enums.entityattribute.UserStatus; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import javax.validation.constraints.Email; - -/** - * @author xclhove - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -@ApiModel(value = "User数据传输对象") -public class UserDTO { - - @TableId - @ApiModelProperty(value = "用户id", example = "1") - private Integer id; - - @ApiModelProperty(value = "用户名", example = "user123") - private String name; - - @ApiModelProperty(value = "账号", example = "user123") - private String account; - - @ApiModelProperty(value = "密码", example = "123456") - private String password; - - @ApiModelProperty(value = "邮箱", example = "123@example.com") - @Email(message = "邮箱格式不正确!") - private String email; - - @ApiModelProperty(value = "用户状态", example = "正常") - private UserStatus status; - - @ApiModelProperty(value = "验证码") - private String verificationCode; -} diff --git a/src/main/java/com/xclhove/xnote/entity/table/Image.java b/src/main/java/com/xclhove/xnote/entity/table/Image.java deleted file mode 100644 index 81cc3ef..0000000 --- a/src/main/java/com/xclhove/xnote/entity/table/Image.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.xclhove.xnote.entity.table; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import lombok.Data; - -import java.sql.Timestamp; - -/** - *

- * 图片表 - *

- * - * @author xclhove - * @since 2023-12-09 - */ -@Data -@TableName("image") -@ApiModel(value="Image对象", description="图片表") -public class Image { - - @ApiModelProperty(value = "图片id") - @TableId(value = "id", type = IdType.AUTO) - private Integer id; - - @ApiModelProperty(value = "用户id") - private Integer userId; - - @ApiModelProperty(value = "图片别名") - private String alias; - - @ApiModelProperty(value = "图片名称") - private String name; - - @ApiModelProperty(value = "上一次下载的时间") - private Timestamp lastDownloadTime; -} diff --git a/src/main/java/com/xclhove/xnote/entity/table/Note.java b/src/main/java/com/xclhove/xnote/entity/table/Note.java deleted file mode 100644 index 12a31b6..0000000 --- a/src/main/java/com/xclhove/xnote/entity/table/Note.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.xclhove.xnote.entity.table; - -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; -import com.xclhove.xnote.enums.entityattribute.NoteIsPublic; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.sql.Timestamp; -import java.util.List; - -/** - * 笔记 - * - * @author xclhove - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -@ApiModel(value = "笔记") -@TableName(autoResultMap = true) -public class Note { - - @TableId - @ApiModelProperty(value = "笔记id", example = "1") - private Integer id; - - @ApiModelProperty(value = "笔记标题", example = "标题") - private String title; - - @ApiModelProperty(value = "笔记内容", example = "笔记内容") - private String content; - - @ApiModelProperty(value = "关键词") - @TableField(typeHandler = JacksonTypeHandler.class) - private List keywords; - - @ApiModelProperty(value = "笔记所属用户id", example = "1") - private Integer userId; - - @ApiModelProperty(value = "笔记发布时间", example = "2020-01-01 00:00:00") - private Timestamp releaseTime; - - @ApiModelProperty(value = "笔记修改时间", example = "2020-01-01 00:00:00") - private Timestamp updateTime; - - @ApiModelProperty(value = "笔记是否公开,未公开时需要访问码才能查看") - private NoteIsPublic isPublic; - - @ApiModelProperty(value = "笔记访问码" , example = "123456") - private String accessCode; -} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/entity/table/User.java b/src/main/java/com/xclhove/xnote/entity/table/User.java deleted file mode 100644 index d5a6b90..0000000 --- a/src/main/java/com/xclhove/xnote/entity/table/User.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.xclhove.xnote.entity.table; - -import com.baomidou.mybatisplus.annotation.TableId; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.xclhove.xnote.enums.entityattribute.UserStatus; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 用户 - * - * @author xclhove - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -@ApiModel(value = "用户") -public class User { - - @TableId - @ApiModelProperty(value = "用户id", example = "1") - private Integer id; - - @ApiModelProperty(value = "用户名", example = "user123") - private String name; - - @ApiModelProperty(value = "账号", example = "user123") - private String account; - - @JsonIgnore - @ApiModelProperty(value = "密码", example = "123456") - private String password; - - @ApiModelProperty(value = "邮箱", example = "123@example.com") - private String email; - - @ApiModelProperty(value = "用户状态", example = "正常") - private UserStatus status; -} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/enums/ResultType.java b/src/main/java/com/xclhove/xnote/enums/ResultType.java new file mode 100644 index 0000000..2965d01 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/enums/ResultType.java @@ -0,0 +1,39 @@ +package com.xclhove.xnote.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author xclhove + */ +@Getter +@AllArgsConstructor +public enum ResultType { + SUCCESS(200, "操作成功!"), + NOT_FOUND(404, "请求路径不存在!"), + FAIL(500, "操作失败!"), + SYSTEM_EXCEPTION(550, "系统异常!"), + IP_FREQUENCY_EXCEPTION(551, "IP访问频率异常!"), + DEVICE_FREQUENCY_EXCEPTION(552, "设备访问频率异常!"), + PARAMETER_VALIDATE_EXCEPTION(553, "参数格式错误!"), + USER_SERVICE_EXCEPTION(600, "用户业务异常!"), + USER_TOKEN_EXCEPTION(601, "用户身份校验失败!"), + IMAGE_SERVICE_EXCEPTION(700, "图片业务异常!"), + NOTE_SERVICE_EXCEPTION(800, "笔记业务异常!"), + VERIFICATION_CODE_EXCEPTION(900, "验证码业务异常!"), + NOTE_TYPE_SERVICE_EXCEPTION(1000, "笔记类型业务异常!"), + SHARE_NOTE_RECORD_SERVICE_EXCEPTION(1100, "笔记分享记录业务异常!"), + USER_IMAGE_SERVICE_EXCEPTION(1200, "用户图片业务异常!"), + ; + private final int status; + private final String message; + + public static ResultType getResultType(int status) { + for (ResultType type : ResultType.values()) { + if (type.status == status) { + return type; + } + } + return null; + } +} diff --git a/src/main/java/com/xclhove/xnote/enums/result/ResultType.java b/src/main/java/com/xclhove/xnote/enums/result/ResultType.java deleted file mode 100644 index 17416aa..0000000 --- a/src/main/java/com/xclhove/xnote/enums/result/ResultType.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.xclhove.xnote.enums.result; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * @author xclhove - */ - -@Getter -@AllArgsConstructor -public enum ResultType { - SUCCESS(200, "操作成功!"), - NOTE_ACCESS_CODE_EXCEPTION(211, "访问码不正确!"), - NOT_FOUND_EXCEPTION(404, "请求路径不存在!"), - ERROR(500, "操作失败!"), - PARAMETER_VALIDATE_EXCEPTION(550, "参数校验异常!"), - IP_FREQUENCY_EXCEPTION(551, "IP访问频率异常!"), - VERIFICATION_CODE_EXCEPTION(552, "验证码错误!"), - OPERATION_FREQUENCY_EXCEPTION(552, "操作频率异常!"), - USER_SERVICE_EXCEPTION(600, "用户业务异常!"), - USER_TOKEN_EXCEPTION(601, "用户token异常!"), - NOTE_SERVICE_EXCEPTION(700, "笔记业务异常!"), - ADMIN_SERVICE_EXCEPTION(800, "管理员业务异常!"), - ADMIN_TOKEN_EXCEPTION(801, "管理员token异常!"), - IMAGE_SERVICE_EXCEPTION(900, "图片业务异常!"), - ; - private final int status; - private final String message; - - public static ResultType getResultType(int status) { - for (ResultType type : ResultType.values()) { - if (type.status == status) { - return type; - } - } - return null; - } -} diff --git a/src/main/java/com/xclhove/xnote/exception/AbstractServiceException.java b/src/main/java/com/xclhove/xnote/exception/AbstractServiceException.java new file mode 100644 index 0000000..46ef476 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/exception/AbstractServiceException.java @@ -0,0 +1,28 @@ +package com.xclhove.xnote.exception; + +import com.xclhove.xnote.enums.ResultType; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * 自定义异常都要继承该类 + * + * @author xclhove + */ +@Getter +@NoArgsConstructor +@AllArgsConstructor +public abstract class AbstractServiceException extends RuntimeException { + private int status; + private String message; + private Object data; + + public AbstractServiceException(String message) { + this(ResultType.FAIL.getStatus(), message, null); + } + + public AbstractServiceException(int status, String message) { + this(status, message, null); + } +} diff --git a/src/main/java/com/xclhove/xnote/exception/AdminServiceException.java b/src/main/java/com/xclhove/xnote/exception/AdminServiceException.java deleted file mode 100644 index cde5e9a..0000000 --- a/src/main/java/com/xclhove/xnote/exception/AdminServiceException.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.xclhove.xnote.exception; - -import com.xclhove.xnote.enums.result.ResultType; - -/** - * 管理员登录异常 - * - * @author xclhove - */ -public class AdminServiceException extends ServiceException { - public AdminServiceException() { - this(ResultType.ADMIN_SERVICE_EXCEPTION.getMessage()); - } - - public AdminServiceException(String message) { - this(message, null); - } - - public AdminServiceException(String message, Object data) { - super(ResultType.ADMIN_SERVICE_EXCEPTION.getStatus(), message, data); - } -} diff --git a/src/main/java/com/xclhove/xnote/exception/AdminTokenException.java b/src/main/java/com/xclhove/xnote/exception/AdminTokenException.java deleted file mode 100644 index 012cacb..0000000 --- a/src/main/java/com/xclhove/xnote/exception/AdminTokenException.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.xclhove.xnote.exception; - -import com.xclhove.xnote.enums.result.ResultType; - -/** - * 管理员token异常 - * - * @author xclhove - */ -public class AdminTokenException extends ServiceException { - public AdminTokenException() { - this(ResultType.ADMIN_TOKEN_EXCEPTION.getMessage()); - } - - public AdminTokenException(String message) { - this(message, null); - } - - public AdminTokenException(String message, Object data) { - super(ResultType.ADMIN_TOKEN_EXCEPTION.getStatus(), message, data); - } -} diff --git a/src/main/java/com/xclhove/xnote/exception/DeviceFrequencyException.java b/src/main/java/com/xclhove/xnote/exception/DeviceFrequencyException.java new file mode 100644 index 0000000..0da9c94 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/exception/DeviceFrequencyException.java @@ -0,0 +1,20 @@ +package com.xclhove.xnote.exception; + +import com.xclhove.xnote.enums.ResultType; + +/** + * @author xclhove + */ +public class DeviceFrequencyException extends AbstractServiceException { + public DeviceFrequencyException() { + this(ResultType.DEVICE_FREQUENCY_EXCEPTION.getMessage()); + } + + public DeviceFrequencyException(String message) { + this(message, null); + } + + public DeviceFrequencyException(String message, Object data) { + super(ResultType.DEVICE_FREQUENCY_EXCEPTION.getStatus(), message, data); + } +} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/exception/ImageServiceException.java b/src/main/java/com/xclhove/xnote/exception/ImageServiceException.java index d18ce63..eeed718 100644 --- a/src/main/java/com/xclhove/xnote/exception/ImageServiceException.java +++ b/src/main/java/com/xclhove/xnote/exception/ImageServiceException.java @@ -1,13 +1,12 @@ package com.xclhove.xnote.exception; -import com.xclhove.xnote.enums.result.ResultType; + +import com.xclhove.xnote.enums.ResultType; /** - * 图片业务异常 - * * @author xclhove */ -public class ImageServiceException extends ServiceException { +public class ImageServiceException extends AbstractServiceException { public ImageServiceException() { this(ResultType.IMAGE_SERVICE_EXCEPTION.getMessage()); } @@ -19,4 +18,4 @@ public ImageServiceException(String message) { public ImageServiceException(String message, Object data) { super(ResultType.IMAGE_SERVICE_EXCEPTION.getStatus(), message, data); } -} \ No newline at end of file +} diff --git a/src/main/java/com/xclhove/xnote/exception/IpFrequencyException.java b/src/main/java/com/xclhove/xnote/exception/IpFrequencyException.java index 1259001..6dbfd45 100644 --- a/src/main/java/com/xclhove/xnote/exception/IpFrequencyException.java +++ b/src/main/java/com/xclhove/xnote/exception/IpFrequencyException.java @@ -1,12 +1,11 @@ package com.xclhove.xnote.exception; -import com.xclhove.xnote.enums.result.ResultType; +import com.xclhove.xnote.enums.ResultType; /** - * IP 频率异常 * @author xclhove */ -public class IpFrequencyException extends ServiceException{ +public class IpFrequencyException extends AbstractServiceException { public IpFrequencyException() { this(ResultType.IP_FREQUENCY_EXCEPTION.getMessage()); } diff --git a/src/main/java/com/xclhove/xnote/exception/NotFoundException.java b/src/main/java/com/xclhove/xnote/exception/NotFoundException.java index 9ecfc29..926e270 100644 --- a/src/main/java/com/xclhove/xnote/exception/NotFoundException.java +++ b/src/main/java/com/xclhove/xnote/exception/NotFoundException.java @@ -1,15 +1,14 @@ package com.xclhove.xnote.exception; -import com.xclhove.xnote.enums.result.ResultType; + +import com.xclhove.xnote.enums.ResultType; /** - * 请求路径不存在异常 - * * @author xclhove */ -public class NotFoundException extends ServiceException { +public class NotFoundException extends AbstractServiceException { public NotFoundException() { - this(ResultType.NOT_FOUND_EXCEPTION.getMessage()); + this(ResultType.NOT_FOUND.getMessage()); } public NotFoundException(String message) { @@ -17,6 +16,6 @@ public NotFoundException(String message) { } public NotFoundException(String message, Object data) { - super(ResultType.NOT_FOUND_EXCEPTION.getStatus(), message, data); + super(ResultType.NOT_FOUND.getStatus(), message, data); } } \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/exception/NoteAccessCodeException.java b/src/main/java/com/xclhove/xnote/exception/NoteAccessCodeException.java deleted file mode 100644 index 4c23214..0000000 --- a/src/main/java/com/xclhove/xnote/exception/NoteAccessCodeException.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.xclhove.xnote.exception; - -import com.xclhove.xnote.enums.result.ResultType; - -/** - * 笔记访问码异常 - * - * @author xclhove - */ -public class NoteAccessCodeException extends ServiceException { - public NoteAccessCodeException() { - this(ResultType.NOTE_ACCESS_CODE_EXCEPTION.getMessage()); - } - - public NoteAccessCodeException(String message) { - this(message, null); - } - - public NoteAccessCodeException(String message, Object data) { - super(ResultType.NOTE_ACCESS_CODE_EXCEPTION.getStatus(), message, data); - } -} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/exception/NoteServiceException.java b/src/main/java/com/xclhove/xnote/exception/NoteServiceException.java index b40ad5b..0bbc4f3 100644 --- a/src/main/java/com/xclhove/xnote/exception/NoteServiceException.java +++ b/src/main/java/com/xclhove/xnote/exception/NoteServiceException.java @@ -1,13 +1,12 @@ package com.xclhove.xnote.exception; -import com.xclhove.xnote.enums.result.ResultType; + +import com.xclhove.xnote.enums.ResultType; /** - * 笔记异常 - * * @author xclhove */ -public class NoteServiceException extends ServiceException { +public class NoteServiceException extends AbstractServiceException { public NoteServiceException() { this(ResultType.NOTE_SERVICE_EXCEPTION.getMessage()); } @@ -19,4 +18,4 @@ public NoteServiceException(String message) { public NoteServiceException(String message, Object data) { super(ResultType.NOTE_SERVICE_EXCEPTION.getStatus(), message, data); } -} \ No newline at end of file +} diff --git a/src/main/java/com/xclhove/xnote/exception/NoteTypeServiceException.java b/src/main/java/com/xclhove/xnote/exception/NoteTypeServiceException.java new file mode 100644 index 0000000..abbf479 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/exception/NoteTypeServiceException.java @@ -0,0 +1,21 @@ +package com.xclhove.xnote.exception; + + +import com.xclhove.xnote.enums.ResultType; + +/** + * @author xclhove + */ +public class NoteTypeServiceException extends AbstractServiceException { + public NoteTypeServiceException() { + this(ResultType.NOTE_TYPE_SERVICE_EXCEPTION.getMessage()); + } + + public NoteTypeServiceException(String message) { + this(message, null); + } + + public NoteTypeServiceException(String message, Object data) { + super(ResultType.NOTE_TYPE_SERVICE_EXCEPTION.getStatus(), message, data); + } +} diff --git a/src/main/java/com/xclhove/xnote/exception/OperationFrequencyException.java b/src/main/java/com/xclhove/xnote/exception/OperationFrequencyException.java deleted file mode 100644 index b6a14d8..0000000 --- a/src/main/java/com/xclhove/xnote/exception/OperationFrequencyException.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.xclhove.xnote.exception; - -import com.xclhove.xnote.enums.result.ResultType; - -/** - * 操作频率异常 - * - * @author xclhove - */ -public class OperationFrequencyException extends ServiceException { - public OperationFrequencyException() { - this(ResultType.OPERATION_FREQUENCY_EXCEPTION.getMessage()); - } - - public OperationFrequencyException(String message) { - this(message, null); - } - - public OperationFrequencyException(String message, Object data) { - super(ResultType.OPERATION_FREQUENCY_EXCEPTION.getStatus(), message, data); - } -} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/exception/ParameterValidateException.java b/src/main/java/com/xclhove/xnote/exception/ParameterValidateException.java index a0d19b8..17134f4 100644 --- a/src/main/java/com/xclhove/xnote/exception/ParameterValidateException.java +++ b/src/main/java/com/xclhove/xnote/exception/ParameterValidateException.java @@ -1,13 +1,12 @@ package com.xclhove.xnote.exception; -import com.xclhove.xnote.enums.result.ResultType; + +import com.xclhove.xnote.enums.ResultType; /** - * 参数校验异常 - * * @author xclhove */ -public class ParameterValidateException extends ServiceException { +public class ParameterValidateException extends AbstractServiceException { public ParameterValidateException() { this(ResultType.PARAMETER_VALIDATE_EXCEPTION.getMessage()); } @@ -19,4 +18,4 @@ public ParameterValidateException(String message) { public ParameterValidateException(String message, Object data) { super(ResultType.PARAMETER_VALIDATE_EXCEPTION.getStatus(), message, data); } -} \ No newline at end of file +} diff --git a/src/main/java/com/xclhove/xnote/exception/ServiceException.java b/src/main/java/com/xclhove/xnote/exception/ServiceException.java deleted file mode 100644 index ce14264..0000000 --- a/src/main/java/com/xclhove/xnote/exception/ServiceException.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.xclhove.xnote.exception; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -/** - * 业务异常 - * - * @author xclhove - */ -@Getter -@NoArgsConstructor -@AllArgsConstructor -public abstract class ServiceException extends RuntimeException { - private int status; - private String message; - private Object data; - - public ServiceException(int status, String message) { - this(status, message, null); - } -} diff --git a/src/main/java/com/xclhove/xnote/exception/ShareNoteRecordServiceException.java b/src/main/java/com/xclhove/xnote/exception/ShareNoteRecordServiceException.java new file mode 100644 index 0000000..533d478 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/exception/ShareNoteRecordServiceException.java @@ -0,0 +1,21 @@ +package com.xclhove.xnote.exception; + + +import com.xclhove.xnote.enums.ResultType; + +/** + * @author xclhove + */ +public class ShareNoteRecordServiceException extends AbstractServiceException { + public ShareNoteRecordServiceException() { + this(ResultType.SHARE_NOTE_RECORD_SERVICE_EXCEPTION.getMessage()); + } + + public ShareNoteRecordServiceException(String message) { + this(message, null); + } + + public ShareNoteRecordServiceException(String message, Object data) { + super(ResultType.SHARE_NOTE_RECORD_SERVICE_EXCEPTION.getStatus(), message, data); + } +} diff --git a/src/main/java/com/xclhove/xnote/exception/SystemException.java b/src/main/java/com/xclhove/xnote/exception/SystemException.java new file mode 100644 index 0000000..a67fa91 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/exception/SystemException.java @@ -0,0 +1,21 @@ +package com.xclhove.xnote.exception; + + +import com.xclhove.xnote.enums.ResultType; + +/** + * @author xclhove + */ +public class SystemException extends AbstractServiceException { + public SystemException() { + this(ResultType.SYSTEM_EXCEPTION.getMessage()); + } + + public SystemException(String message) { + this(message, null); + } + + public SystemException(String message, Object data) { + super(ResultType.SYSTEM_EXCEPTION.getStatus(), message, data); + } +} diff --git a/src/main/java/com/xclhove/xnote/exception/UserImageServiceException.java b/src/main/java/com/xclhove/xnote/exception/UserImageServiceException.java new file mode 100644 index 0000000..5a9ae35 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/exception/UserImageServiceException.java @@ -0,0 +1,21 @@ +package com.xclhove.xnote.exception; + + +import com.xclhove.xnote.enums.ResultType; + +/** + * @author xclhove + */ +public class UserImageServiceException extends AbstractServiceException { + public UserImageServiceException() { + this(ResultType.USER_IMAGE_SERVICE_EXCEPTION.getMessage()); + } + + public UserImageServiceException(String message) { + this(message, null); + } + + public UserImageServiceException(String message, Object data) { + super(ResultType.USER_IMAGE_SERVICE_EXCEPTION.getStatus(), message, data); + } +} diff --git a/src/main/java/com/xclhove/xnote/exception/UserServiceException.java b/src/main/java/com/xclhove/xnote/exception/UserServiceException.java index 5eafe96..aa3d1f1 100644 --- a/src/main/java/com/xclhove/xnote/exception/UserServiceException.java +++ b/src/main/java/com/xclhove/xnote/exception/UserServiceException.java @@ -1,13 +1,12 @@ package com.xclhove.xnote.exception; -import com.xclhove.xnote.enums.result.ResultType; + +import com.xclhove.xnote.enums.ResultType; /** - * 用户业务异常 - * * @author xclhove */ -public class UserServiceException extends ServiceException { +public class UserServiceException extends AbstractServiceException { public UserServiceException() { this(ResultType.USER_SERVICE_EXCEPTION.getMessage()); } @@ -19,4 +18,4 @@ public UserServiceException(String message) { public UserServiceException(String message, Object data) { super(ResultType.USER_SERVICE_EXCEPTION.getStatus(), message, data); } -} \ No newline at end of file +} diff --git a/src/main/java/com/xclhove/xnote/exception/UserTokenException.java b/src/main/java/com/xclhove/xnote/exception/UserTokenException.java index 958c783..f8ea6a5 100644 --- a/src/main/java/com/xclhove/xnote/exception/UserTokenException.java +++ b/src/main/java/com/xclhove/xnote/exception/UserTokenException.java @@ -1,13 +1,12 @@ package com.xclhove.xnote.exception; -import com.xclhove.xnote.enums.result.ResultType; + +import com.xclhove.xnote.enums.ResultType; /** - * 用户token异常 - * * @author xclhove */ -public class UserTokenException extends ServiceException { +public class UserTokenException extends AbstractServiceException { public UserTokenException() { this(ResultType.USER_TOKEN_EXCEPTION.getMessage()); } diff --git a/src/main/java/com/xclhove/xnote/exception/VerificationCodeException.java b/src/main/java/com/xclhove/xnote/exception/VerificationCodeException.java index a15f656..5d3d511 100644 --- a/src/main/java/com/xclhove/xnote/exception/VerificationCodeException.java +++ b/src/main/java/com/xclhove/xnote/exception/VerificationCodeException.java @@ -1,13 +1,12 @@ package com.xclhove.xnote.exception; -import com.xclhove.xnote.enums.result.ResultType; + +import com.xclhove.xnote.enums.ResultType; /** - * 操作频率异常 - * * @author xclhove */ -public class VerificationCodeException extends ServiceException { +public class VerificationCodeException extends AbstractServiceException { public VerificationCodeException() { this(ResultType.VERIFICATION_CODE_EXCEPTION.getMessage()); } @@ -19,4 +18,4 @@ public VerificationCodeException(String message) { public VerificationCodeException(String message, Object data) { super(ResultType.VERIFICATION_CODE_EXCEPTION.getStatus(), message, data); } -} \ No newline at end of file +} diff --git a/src/main/java/com/xclhove/xnote/interceptor/DeviceInterceptor.java b/src/main/java/com/xclhove/xnote/interceptor/DeviceInterceptor.java new file mode 100644 index 0000000..4197285 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/interceptor/DeviceInterceptor.java @@ -0,0 +1,80 @@ +package com.xclhove.xnote.interceptor; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.SecureUtil; +import com.xclhove.xnote.config.XnoteConfig; +import com.xclhove.xnote.constant.RedisKey; +import com.xclhove.xnote.constant.RequestHeaderKey; +import com.xclhove.xnote.constant.TreadLocalKey; +import com.xclhove.xnote.exception.DeviceFrequencyException; +import com.xclhove.xnote.util.ThreadLocalUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.core.annotation.Order; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +/** + * @author xclhove + */ +@Component +@RequiredArgsConstructor +@Order(0) +public class DeviceInterceptor extends ServiceInterceptor { + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface DeviceIntercept { + /** + * 设为false则不会进行拦截 + */ + boolean needIntercept() default true; + } + + private final StringRedisTemplate stringRedisTemplate; + private final XnoteConfig xnoteConfig; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String userAgent = request.getHeader(RequestHeaderKey.USER_AGENT); + String deviceId = SecureUtil.md5(userAgent); + ThreadLocalUtil.set(TreadLocalKey.DEVICE_ID, deviceId); + + XnoteConfig.Interceptor.Device device = xnoteConfig.interceptor.device; + if (device.getDisable()) { + return true; + } + + //如果不是映射到方法 + if (!(handler instanceof HandlerMethod)) { + return true; + } + + HandlerMethod handlerMethod = (HandlerMethod) handler; + DeviceIntercept intercept = handlerMethod.getMethod().getAnnotation(DeviceIntercept.class); + if (intercept != null && !intercept.needIntercept()) { + return true; + } + + int crruentRequency = 0; + String redisKey = RedisKey.join(RedisKey.Interceptor.DEVICE, deviceId); + String lastFrequency = stringRedisTemplate.opsForValue().get(redisKey); + if (StrUtil.isNotBlank(lastFrequency)) { + crruentRequency = Integer.parseInt(lastFrequency); + } + crruentRequency++; + if (crruentRequency > device.getMaxFrequencyPerMinute()) { + throw new DeviceFrequencyException(); + } + stringRedisTemplate.opsForValue().set(redisKey, String.valueOf(crruentRequency), 1, TimeUnit.MINUTES); + ThreadLocalUtil.set(TreadLocalKey.DEVICE_ID, deviceId); + return true; + } +} diff --git a/src/main/java/com/xclhove/xnote/interceptor/IpInterceptor.java b/src/main/java/com/xclhove/xnote/interceptor/IpInterceptor.java new file mode 100644 index 0000000..f8e32a0 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/interceptor/IpInterceptor.java @@ -0,0 +1,107 @@ +package com.xclhove.xnote.interceptor; + +import cn.hutool.core.util.StrUtil; +import com.xclhove.xnote.config.XnoteConfig; +import com.xclhove.xnote.constant.RedisKey; +import com.xclhove.xnote.exception.IpFrequencyException; +import com.xclhove.xnote.tool.ThreadLocalTool; +import com.xclhove.xnote.util.RequestUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +/** + * @author xclhove + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class IpInterceptor extends ServiceInterceptor { + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface PathFrequencyLimit { + /** + * 注解所在位置的请求路径的单个ip的每分钟最大请求次数 + */ + int maxFrequencyPerMinute() default 0; + /** + * 警告信息 + */ + String message() default ""; + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface DisableIpInterceptor {} + + private final StringRedisTemplate stringRedisTemplate; + private final XnoteConfig xnoteConfig; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String clientIp = RequestUtil.getClientIpAddress(request); + ThreadLocalTool.setClientIp(clientIp); + String requestPath = RequestUtil.getCurrentPath(request); + + //如果不是映射到方法 + if (!(handler instanceof HandlerMethod)) { + return true; + } + HandlerMethod handlerMethod = (HandlerMethod) handler; + + // 拦截特定请求路径 + PathFrequencyLimit pathFrequencyLimit = handlerMethod.getMethod().getAnnotation(PathFrequencyLimit.class); + if (pathFrequencyLimit != null) { + intercept( + RedisKey.join(clientIp, requestPath), + pathFrequencyLimit.maxFrequencyPerMinute(), + pathFrequencyLimit.message() + ); + } + + XnoteConfig.Interceptor.Ip ip = xnoteConfig.interceptor.ip; + if (ip.getDisable()) { + return true; + } + DisableIpInterceptor disableIpInterceptor = handlerMethod.getMethod().getAnnotation(DisableIpInterceptor.class); + if (disableIpInterceptor != null) { + return true; + } + intercept(clientIp, ip.getMaxFrequencyPerMinute(), null); + + return true; + } + + private void intercept(String key, int maxFrequencyPerMinute, String exceptionMessage) throws IpFrequencyException { + if (maxFrequencyPerMinute <= 0) { + return; + } + + int crruentRequency = 0; + String redisKey = RedisKey.join(RedisKey.Interceptor.IP, key); + String lastFrequency = stringRedisTemplate.opsForValue().get(redisKey); + if (StrUtil.isNotBlank(lastFrequency)) { + crruentRequency = Integer.parseInt(lastFrequency); + } + crruentRequency++; + stringRedisTemplate.opsForValue().increment(redisKey); + if (crruentRequency <= maxFrequencyPerMinute) { + stringRedisTemplate.opsForValue().set(redisKey, String.valueOf(crruentRequency), 1, TimeUnit.MINUTES); + return; + } + if (StrUtil.isNotBlank(exceptionMessage)) { + throw new IpFrequencyException(exceptionMessage); + } + throw new IpFrequencyException(); + } +} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/Interceptor/ServiceInterceptor.java b/src/main/java/com/xclhove/xnote/interceptor/ServiceInterceptor.java similarity index 93% rename from src/main/java/com/xclhove/xnote/Interceptor/ServiceInterceptor.java rename to src/main/java/com/xclhove/xnote/interceptor/ServiceInterceptor.java index 6bcceef..b42c126 100644 --- a/src/main/java/com/xclhove/xnote/Interceptor/ServiceInterceptor.java +++ b/src/main/java/com/xclhove/xnote/interceptor/ServiceInterceptor.java @@ -1,4 +1,4 @@ -package com.xclhove.xnote.Interceptor; +package com.xclhove.xnote.interceptor; import com.xclhove.xnote.util.ThreadLocalUtil; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/xclhove/xnote/interceptor/UserTokenInterceptor.java b/src/main/java/com/xclhove/xnote/interceptor/UserTokenInterceptor.java new file mode 100644 index 0000000..3ada9cd --- /dev/null +++ b/src/main/java/com/xclhove/xnote/interceptor/UserTokenInterceptor.java @@ -0,0 +1,97 @@ +package com.xclhove.xnote.interceptor; + +import cn.hutool.core.util.StrUtil; +import com.xclhove.xnote.constant.RequestHeaderKey; +import com.xclhove.xnote.constant.TreadLocalKey; +import com.xclhove.xnote.exception.UserTokenException; +import com.xclhove.xnote.pojo.enums.UserStatus; +import com.xclhove.xnote.pojo.table.User; +import com.xclhove.xnote.service.UserService; +import com.xclhove.xnote.tool.ThreadLocalTool; +import com.xclhove.xnote.tool.UserTokenTool; +import com.xclhove.xnote.util.ThreadLocalUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.time.Duration; + +/** + * @author xclhove + */ +@Slf4j +@Component +@RequiredArgsConstructor +@Order +public class UserTokenInterceptor extends ServiceInterceptor { + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface UserTokenIntercept { + boolean needIntercept() default true; + } + + private final UserService userService; + private final StringRedisTemplate stringRedisTemplate; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + //如果不是映射到方法直接通过 + if (!(handler instanceof HandlerMethod)) { + return true; + } + + try { + String token = request.getHeader(RequestHeaderKey.AUTHORIZATION); + if (StrUtil.isBlank(token)) { + throw new UserTokenException(); + } + + Integer userId = UserTokenTool.getUserId(token); + if (userId == null) { + throw new UserTokenException(); + } + + String deviceId = ThreadLocalUtil.get(TreadLocalKey.DEVICE_ID, String.class); + String redisKey = userService.getTokenRedisKey(userId, deviceId); + String tokenInRedis = stringRedisTemplate.opsForValue().get(redisKey); + if (!token.equals(tokenInRedis)) { + throw new UserTokenException(); + } + + User user = userService.getByIdWithRedis(userId); + if (user == null) { + throw new UserTokenException(); + } + + // 更新 token 过期时间 + stringRedisTemplate.expire(redisKey, Duration.ofHours(2)); + + if (user.getStatus() == UserStatus.DISABLE) { + throw new UserTokenException("账号已被禁用"); + } + + ThreadLocalTool.setUser(user); + return true; + } catch (UserTokenException userTokenException) { + HandlerMethod handlerMethod = (HandlerMethod) handler; + UserTokenIntercept userTokenIntercept = handlerMethod.getMethod().getAnnotation(UserTokenIntercept.class); + //如果要访问的方法上没有加这个注解,那么就说明这个方法不需要拦截 + if(userTokenIntercept == null) { + return true; + } + if (!userTokenIntercept.needIntercept()) { + return true; + } + throw userTokenException; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/mapper/AdminMapper.java b/src/main/java/com/xclhove/xnote/mapper/AdminMapper.java deleted file mode 100644 index 5e4a033..0000000 --- a/src/main/java/com/xclhove/xnote/mapper/AdminMapper.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.xclhove.xnote.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.xclhove.xnote.entity.table.Admin; -import org.apache.ibatis.annotations.Mapper; - -/** - * @author xclhove - */ -@Mapper -public interface AdminMapper extends BaseMapper { -} diff --git a/src/main/java/com/xclhove/xnote/mapper/ImageMapper.java b/src/main/java/com/xclhove/xnote/mapper/ImageMapper.java index bca51b5..d9cf9f9 100644 --- a/src/main/java/com/xclhove/xnote/mapper/ImageMapper.java +++ b/src/main/java/com/xclhove/xnote/mapper/ImageMapper.java @@ -1,18 +1,14 @@ package com.xclhove.xnote.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.xclhove.xnote.entity.table.Image; -import org.apache.ibatis.annotations.Mapper; +import com.xclhove.xnote.pojo.table.Image; +import org.apache.ibatis.annotations.Param; + +import java.util.List; /** - *

- * 图片表 Mapper 接口 - *

- * * @author xclhove - * @since 2023-12-09 */ -@Mapper public interface ImageMapper extends BaseMapper { - + boolean incrementImageOwnerCount(@Param("userImageIds") List userImageIds, @Param("incrementStep") Integer incrementStep); } diff --git a/src/main/java/com/xclhove/xnote/mapper/NoteMapper.java b/src/main/java/com/xclhove/xnote/mapper/NoteMapper.java index 426e282..5c83c61 100644 --- a/src/main/java/com/xclhove/xnote/mapper/NoteMapper.java +++ b/src/main/java/com/xclhove/xnote/mapper/NoteMapper.java @@ -1,12 +1,20 @@ package com.xclhove.xnote.mapper; +import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.xclhove.xnote.entity.table.Note; -import org.apache.ibatis.annotations.Mapper; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.xclhove.xnote.pojo.table.Note; +import org.apache.ibatis.annotations.Param; /** * @author xclhove */ -@Mapper public interface NoteMapper extends BaseMapper { + Page search( + @Param("userId") Integer userId, + @Param("search") String search, + @Param("page") Page page, + @Param(Constants.WRAPPER) Wrapper queryWrapper + ); } diff --git a/src/main/java/com/xclhove/xnote/mapper/NoteTypeMapper.java b/src/main/java/com/xclhove/xnote/mapper/NoteTypeMapper.java new file mode 100644 index 0000000..5e2f9dd --- /dev/null +++ b/src/main/java/com/xclhove/xnote/mapper/NoteTypeMapper.java @@ -0,0 +1,11 @@ +package com.xclhove.xnote.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.xclhove.xnote.pojo.table.NoteType; + +/** + * @author xclhove + */ +public interface NoteTypeMapper extends BaseMapper { + +} diff --git a/src/main/java/com/xclhove/xnote/mapper/ShareNoteRecordMapper.java b/src/main/java/com/xclhove/xnote/mapper/ShareNoteRecordMapper.java new file mode 100644 index 0000000..686a2f3 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/mapper/ShareNoteRecordMapper.java @@ -0,0 +1,32 @@ +package com.xclhove.xnote.mapper; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.xclhove.xnote.pojo.table.ShareNoteRecord; +import com.xclhove.xnote.pojo.vo.ShareNoteRecordVO; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +/** + * @author xclhove + */ +public interface ShareNoteRecordMapper extends BaseMapper { + + @Select("select SNR.id, SNR.user_id, SNR.code, SNR.expire_time,SNR.note_id, N.title " + + "from (select * from share_note_record where user_id = #{userId}) SNR, " + + "(select id, title from note where user_id = #{userId}) N " + + "where SNR.note_id = N.id " + + "order by SNR.expire_time desc " + + "${ew.customSqlSegment}" + ) + Page pageUserShareNote( + @Param("userId") + Integer userId, + @Param("page") + Page page, + @Param(Constants.WRAPPER) + Wrapper wrapper + ); +} diff --git a/src/main/java/com/xclhove/xnote/mapper/UserImageMapper.java b/src/main/java/com/xclhove/xnote/mapper/UserImageMapper.java new file mode 100644 index 0000000..36f26c9 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/mapper/UserImageMapper.java @@ -0,0 +1,10 @@ +package com.xclhove.xnote.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.xclhove.xnote.pojo.table.UserImage; + +/** + * @author xclhove + */ +public interface UserImageMapper extends BaseMapper { +} diff --git a/src/main/java/com/xclhove/xnote/mapper/UserMapper.java b/src/main/java/com/xclhove/xnote/mapper/UserMapper.java index 724f641..57edafa 100644 --- a/src/main/java/com/xclhove/xnote/mapper/UserMapper.java +++ b/src/main/java/com/xclhove/xnote/mapper/UserMapper.java @@ -1,12 +1,11 @@ package com.xclhove.xnote.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.xclhove.xnote.entity.table.User; -import org.apache.ibatis.annotations.Mapper; +import com.xclhove.xnote.pojo.table.User; /** * @author xclhove */ -@Mapper public interface UserMapper extends BaseMapper { + } diff --git a/src/main/java/com/xclhove/xnote/mybatisplus/MybatisPlusPrintSqlInterceptor.java b/src/main/java/com/xclhove/xnote/mybatisplus/MybatisPlusPrintSqlInterceptor.java new file mode 100644 index 0000000..1573e60 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/mybatisplus/MybatisPlusPrintSqlInterceptor.java @@ -0,0 +1,197 @@ +package com.xclhove.xnote.mybatisplus; + +import cn.hutool.core.date.DateUtil; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.cache.CacheKey; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.ParameterMapping; +import org.apache.ibatis.mapping.ParameterMode; +import org.apache.ibatis.plugin.*; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; +import org.apache.ibatis.type.TypeHandlerRegistry; + +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * 基于Mybatis Plus的SQL输出拦截器。 + * 完美的输出打印 SQL 及执行时长、statement。 + * 注意:该插件有性能损耗,不建议生产环境使用。 + * @author xclhove + */ +@Slf4j +@Data +@NoArgsConstructor +@AllArgsConstructor +@Intercepts(value = { + @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}), + @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), + @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})}) +public class MybatisPlusPrintSqlInterceptor implements Interceptor { + + /** + * 是否输出到控制台 + */ + private boolean outConsole = false; + + @Override + public Object intercept(Invocation invocation) throws Throwable { + MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; + Object parameter = null; + if (invocation.getArgs().length > 1) { + parameter = invocation.getArgs()[1]; + } + String statement = mappedStatement.getId(); + BoundSql boundSql = mappedStatement.getBoundSql(parameter); + Configuration configuration = mappedStatement.getConfiguration(); + long start = System.currentTimeMillis(); + Object returnValue = invocation.proceed(); + long time = System.currentTimeMillis() - start; + showSql(configuration, boundSql, time, statement); + return returnValue; + } + + @Override + public Object plugin(Object target) { + return Plugin.wrap(target, this); + } + + private void showSql(Configuration configuration, BoundSql boundSql, long elapsed, String statement) { + String logText = formatMessage(DateUtil.now(), elapsed, getSqlWithValues(boundSql.getSql(), buildParameterValues(configuration, boundSql)), statement); + if (Boolean.TRUE == outConsole) { + // 打印红色 SQL 日志 + System.err.println(logText); + } else { + log.info("\n{}", logText); + } + } + + // com.baomidou.mybatisplus.core.MybatisParameterHandler#setParameters + private static Map buildParameterValues(Configuration configuration, BoundSql boundSql) { + Object parameterObject = boundSql.getParameterObject(); + List parameterMappings = boundSql.getParameterMappings(); + if (parameterMappings != null) { + Map parameterValues = new HashMap<>(); + TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); + for (int i = 0; i < parameterMappings.size(); i++) { + ParameterMapping parameterMapping = parameterMappings.get(i); + if (parameterMapping.getMode() != ParameterMode.OUT) { + Object value; + String propertyName = parameterMapping.getProperty(); + if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params + value = boundSql.getAdditionalParameter(propertyName); + } else if (parameterObject == null) { + value = null; + } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { + value = parameterObject; + } else { + MetaObject metaObject = configuration.newMetaObject(parameterObject); + value = metaObject.getValue(propertyName); + } + parameterValues.put(i, new Value(value)); + } + } + return parameterValues; + } + return Collections.emptyMap(); + } + + public static String formatMessage(String now, long elapsed, String sql, String statement) { + return StringUtils.isNotBlank(sql) ? " Consume Time:" + elapsed + " ms " + now + " (" + statement + ")" + + "\n Execute SQL:" + sql.replaceAll("[\\s]+", " ") + "\n" : ""; + } + + public static String getSqlWithValues(String statementQuery, Map parameterValues) { + final StringBuilder sb = new StringBuilder(); + + // iterate over the characters in the query replacing the parameter placeholders + // with the actual values + int currentParameter = 0; + for (int pos = 0; pos < statementQuery.length(); pos++) { + char character = statementQuery.charAt(pos); + if (statementQuery.charAt(pos) == '?' && currentParameter <= parameterValues.size()) { + // replace with parameter value + Object value = parameterValues.get(currentParameter); + sb.append(value != null ? value.toString() : new Value().toString()); + currentParameter++; + } else { + sb.append(character); + } + } + + return sb.toString(); + } + + /** + * 基于p6spy的简易数据类型转换类。 + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Value { + public static final String NORM_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; + public static final String DATABASE_DIALECT_DATE_FORMAT = NORM_DATETIME_PATTERN; + public static final String DATABASE_DIALECT_TIMESTAMP_FORMAT = NORM_DATETIME_PATTERN; + public static final String DATABASE_DIALECT_BOOLEAN_FORMAT = "numeric"; + + private Object value; + + @Override + public String toString() { + return convertToString(this.value); + } + + public String convertToString(Object value) { + if (value == null) { + return "NULL"; + } + + String result; + if (value instanceof byte[]) { + result = new String((byte[]) value); + } else if (value instanceof Timestamp) { + result = new SimpleDateFormat(DATABASE_DIALECT_TIMESTAMP_FORMAT).format(value); + } else if (value instanceof Date) { + result = new SimpleDateFormat(DATABASE_DIALECT_DATE_FORMAT).format(value); + } else if (value instanceof Boolean) { + if ("numeric".equals(DATABASE_DIALECT_BOOLEAN_FORMAT)) { + result = Boolean.FALSE.equals(value) ? "0" : "1"; + } else { + result = value.toString(); + } + } else { + result = value.toString(); + } + result = quoteIfNeeded(result, value); + return result; + } + + private String quoteIfNeeded(String stringValue, Object obj) { + if (stringValue == null) { + return null; + } + if (Number.class.isAssignableFrom(obj.getClass()) || Boolean.class.isAssignableFrom(obj.getClass())) { + return stringValue; + } else { + return "'" + escape(stringValue) + "'"; + } + } + + private String escape(String stringValue) { + return stringValue.replaceAll("'", "''"); + } + + } +} + + diff --git a/src/main/java/com/xclhove/xnote/pojo/dto/UserDTO.java b/src/main/java/com/xclhove/xnote/pojo/dto/UserDTO.java new file mode 100644 index 0000000..0a83fd0 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/dto/UserDTO.java @@ -0,0 +1,40 @@ +package com.xclhove.xnote.pojo.dto; + +import com.xclhove.xnote.pojo.enums.UserStatus; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.Email; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Positive; +import java.io.Serializable; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class UserDTO implements Serializable { + private static final long serialVersionUID = 1L; + @Positive + private Integer id; + @Pattern(regexp = "^\\S{2,30}$", message = "用户名只能为2-30位的非空字符") + private String name; + @Pattern(regexp = "^[a-zA-Z0-9]{2,30}$", message = "账号只能为2-30位的字母和数字") + private String account; + @Pattern(regexp = "^[a-zA-Z\\d._*]{6,30}$", message = "密码只能为6-30位的数字、字母、下划线、点和星号") + private String password; + @Email(message = "email格式不正确") + private String email; + private UserStatus status; + @Positive + private Integer homePageNoteId; + @Positive + private Long imageStorageSize; +} diff --git a/src/main/java/com/xclhove/xnote/enums/entityattribute/NoteIsPublic.java b/src/main/java/com/xclhove/xnote/pojo/enums/NoteIsPublic.java similarity index 89% rename from src/main/java/com/xclhove/xnote/enums/entityattribute/NoteIsPublic.java rename to src/main/java/com/xclhove/xnote/pojo/enums/NoteIsPublic.java index 4a58880..67266f7 100644 --- a/src/main/java/com/xclhove/xnote/enums/entityattribute/NoteIsPublic.java +++ b/src/main/java/com/xclhove/xnote/pojo/enums/NoteIsPublic.java @@ -1,4 +1,4 @@ -package com.xclhove.xnote.enums.entityattribute; +package com.xclhove.xnote.pojo.enums; import com.baomidou.mybatisplus.annotation.EnumValue; import com.fasterxml.jackson.annotation.JsonValue; diff --git a/src/main/java/com/xclhove/xnote/enums/entityattribute/UserStatus.java b/src/main/java/com/xclhove/xnote/pojo/enums/UserStatus.java similarity index 75% rename from src/main/java/com/xclhove/xnote/enums/entityattribute/UserStatus.java rename to src/main/java/com/xclhove/xnote/pojo/enums/UserStatus.java index 014bc3b..0a6f9a7 100644 --- a/src/main/java/com/xclhove/xnote/enums/entityattribute/UserStatus.java +++ b/src/main/java/com/xclhove/xnote/pojo/enums/UserStatus.java @@ -1,4 +1,4 @@ -package com.xclhove.xnote.enums.entityattribute; +package com.xclhove.xnote.pojo.enums; import com.baomidou.mybatisplus.annotation.EnumValue; import com.fasterxml.jackson.annotation.JsonValue; @@ -6,14 +6,16 @@ import lombok.Getter; /** + * 用户状态 + * * @author xclhove */ - @Getter @AllArgsConstructor public enum UserStatus { - NORMAL(1, "正常"), - BANED(2, "已禁封"); + DISABLE(0, "禁用"), + ENABLE(1, "启用"), + ; @EnumValue private final int status; @JsonValue diff --git a/src/main/java/com/xclhove/xnote/pojo/es/NoteDoc.java b/src/main/java/com/xclhove/xnote/pojo/es/NoteDoc.java new file mode 100644 index 0000000..d5c3897 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/es/NoteDoc.java @@ -0,0 +1,83 @@ +package com.xclhove.xnote.pojo.es; + +import cn.hutool.core.bean.BeanUtil; +import com.xclhove.xnote.constant.EsConstant; +import com.xclhove.xnote.pojo.enums.NoteIsPublic; +import com.xclhove.xnote.pojo.table.Note; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; + +import java.io.Serializable; +import java.util.Date; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +@Document(indexName = "note") +public class NoteDoc implements Serializable { + @Id + private Integer id; + + @Field(type = FieldType.Text, + analyzer = EsConstant.Analyzer.IK_MAX_WORD, + searchAnalyzer = EsConstant.Analyzer.IK_MAX_WORD, + copyTo = EsConstant.NOTE_QUERY_FIELD + ) + private String title; + + @Field(type = FieldType.Text, + analyzer = EsConstant.Analyzer.IK_MAX_WORD, + searchAnalyzer = EsConstant.Analyzer.IK_MAX_WORD, + copyTo = EsConstant.NOTE_QUERY_FIELD + ) + private String content; + + @Field(type = FieldType.Keyword) + private Integer userId; + + @Field(type = FieldType.Date) + private Date releaseTime; + + @Field(type = FieldType.Date) + private Date updateTime; + + @Field(type = FieldType.Keyword) + private NoteIsPublic isPublic; + + @Field(type = FieldType.Keyword) + private Integer typeId; + + //@CompletionField( + // analyzer = EsConstant.Analyzer.IK_MAX_WORD, + // searchAnalyzer = EsConstant.Analyzer.IK_MAX_WORD + //) + //private String suggestionField; + + public Note toNote() { + return BeanUtil.copyProperties(this, Note.class); + } + + //public NoteDoc generateSuggestionField() { + // suggestionField = ""; + // if (title != null) { + // suggestionField += title; + // } + // if (content != null) { + // suggestionField += content; + // } + // + // return this; + //} +} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/pojo/form/note/NoteAddForm.java b/src/main/java/com/xclhove/xnote/pojo/form/note/NoteAddForm.java new file mode 100644 index 0000000..0259873 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/form/note/NoteAddForm.java @@ -0,0 +1,35 @@ +package com.xclhove.xnote.pojo.form.note; + +import com.xclhove.xnote.pojo.enums.NoteIsPublic; +import com.xclhove.xnote.pojo.form.rule.NoteFormRule; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Positive; +import javax.validation.constraints.Size; +import java.io.Serializable; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class NoteAddForm implements Serializable { + @NotBlank(message = "标题不能为空") + @Size(min = NoteFormRule.Title.MIN, max = NoteFormRule.Title.MAX, message = NoteFormRule.Title.MESSAGE) + private String title; + @NotBlank(message = "内容不能为空") + private String content; + @NotNull(message = "请选择是否公开笔记") + private NoteIsPublic isPublic; + @Positive(message = "笔记类型ID必须为正整数") + private Integer typeId; +} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/pojo/form/note/NoteUpdateForm.java b/src/main/java/com/xclhove/xnote/pojo/form/note/NoteUpdateForm.java new file mode 100644 index 0000000..6b793b3 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/form/note/NoteUpdateForm.java @@ -0,0 +1,33 @@ +package com.xclhove.xnote.pojo.form.note; + +import com.xclhove.xnote.pojo.enums.NoteIsPublic; +import com.xclhove.xnote.pojo.form.rule.NoteFormRule; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Positive; +import javax.validation.constraints.Size; +import java.io.Serializable; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class NoteUpdateForm implements Serializable { + @NotNull(message = "笔记ID不能为空") + @Positive(message = "笔记ID必须为正整数") + private Integer id; + @Size(min = NoteFormRule.Title.MIN, max = NoteFormRule.Title.MAX, message = NoteFormRule.Title.MESSAGE) + private String title; + private String content; + private NoteIsPublic isPublic; + private Integer typeId; +} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/pojo/form/note/NoteUpdateTypeForm.java b/src/main/java/com/xclhove/xnote/pojo/form/note/NoteUpdateTypeForm.java new file mode 100644 index 0000000..b56f386 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/form/note/NoteUpdateTypeForm.java @@ -0,0 +1,27 @@ +package com.xclhove.xnote.pojo.form.note; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Positive; +import java.util.List; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class NoteUpdateTypeForm { + @NotNull + private List noteIds; + @NotNull + @Positive + private Integer typeId; +} diff --git a/src/main/java/com/xclhove/xnote/pojo/form/noteType/NoteTypeAddForm.java b/src/main/java/com/xclhove/xnote/pojo/form/noteType/NoteTypeAddForm.java new file mode 100644 index 0000000..af4cacf --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/form/noteType/NoteTypeAddForm.java @@ -0,0 +1,26 @@ +package com.xclhove.xnote.pojo.form.noteType; + +import com.xclhove.xnote.pojo.form.rule.NoteTypeFormRule; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import java.io.Serializable; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class NoteTypeAddForm implements Serializable { + @NotBlank(message = "类型名称不能为空") + @Pattern(regexp = NoteTypeFormRule.Name.REGEXP, message = NoteTypeFormRule.Name.MESSAGE) + private String name; +} diff --git a/src/main/java/com/xclhove/xnote/pojo/form/noteType/NoteTypeUpdateForm.java b/src/main/java/com/xclhove/xnote/pojo/form/noteType/NoteTypeUpdateForm.java new file mode 100644 index 0000000..f786164 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/form/noteType/NoteTypeUpdateForm.java @@ -0,0 +1,31 @@ +package com.xclhove.xnote.pojo.form.noteType; + +import com.xclhove.xnote.pojo.form.rule.NoteTypeFormRule; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Positive; +import java.io.Serializable; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class NoteTypeUpdateForm implements Serializable { + @NotNull(message = "类型ID不能为空") + @Positive(message = "类型ID必须为正整数") + private Integer id; + @NotBlank(message = "类型名称不能为空") + @Pattern(regexp = NoteTypeFormRule.Name.REGEXP, message = NoteTypeFormRule.Name.MESSAGE) + private String name; +} diff --git a/src/main/java/com/xclhove/xnote/pojo/form/rule/NoteFormRule.java b/src/main/java/com/xclhove/xnote/pojo/form/rule/NoteFormRule.java new file mode 100644 index 0000000..2d3743d --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/form/rule/NoteFormRule.java @@ -0,0 +1,12 @@ +package com.xclhove.xnote.pojo.form.rule; + +/** + * @author xclhove + */ +public interface NoteFormRule { + interface Title { + int MIN = 1; + int MAX = 200; + String MESSAGE = "标题长度必须在" + MIN + "-" + MAX + "个字符之间"; + } +} diff --git a/src/main/java/com/xclhove/xnote/pojo/form/rule/NoteTypeFormRule.java b/src/main/java/com/xclhove/xnote/pojo/form/rule/NoteTypeFormRule.java new file mode 100644 index 0000000..e6c64b4 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/form/rule/NoteTypeFormRule.java @@ -0,0 +1,11 @@ +package com.xclhove.xnote.pojo.form.rule; + +/** + * @author xclhove + */ +public interface NoteTypeFormRule { + interface Name { + String REGEXP = "^\\S{1,10}$"; + String MESSAGE = "类型名称只能为1-10位的非空字符"; + } +} diff --git a/src/main/java/com/xclhove/xnote/pojo/form/rule/UserFormRule.java b/src/main/java/com/xclhove/xnote/pojo/form/rule/UserFormRule.java new file mode 100644 index 0000000..3d693a1 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/form/rule/UserFormRule.java @@ -0,0 +1,23 @@ +package com.xclhove.xnote.pojo.form.rule; + +/** + * @author xclhove + */ +public interface UserFormRule { + interface Name { + String REGEXP = "^\\S{2,30}$"; + String MESSAGE = "用户名只能为2-30位的非空字符"; + } + interface Account { + String REGEXP = "^[a-zA-Z0-9]{2,30}$"; + String MESSAGE = "账号只能为2-30位的字母和数字"; + } + interface Password { + String REGEXP = "^[a-zA-Z\\d._*]{6,30}$"; + String MESSAGE = "密码只能为6-30位的数字、字母、下划线、点和星号"; + } + interface Email { + String REGEXP = "^[\\w-]+@((\\w+)\\.)+([a-zA-Z]{2,4})$"; + String MESSAGE = "邮箱格式不正确"; + } +} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/pojo/form/shareNoteRecord/ShareNoteRecordCreateForm.java b/src/main/java/com/xclhove/xnote/pojo/form/shareNoteRecord/ShareNoteRecordCreateForm.java new file mode 100644 index 0000000..aadcd5c --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/form/shareNoteRecord/ShareNoteRecordCreateForm.java @@ -0,0 +1,30 @@ +package com.xclhove.xnote.pojo.form.shareNoteRecord; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Positive; +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class ShareNoteRecordCreateForm implements Serializable { + @NotNull(message = "笔记ID不能为空") + @Positive(message = "笔记ID必须为正整数") + private Integer noteId; + /** + * 默认为7天后过期 + */ + private Timestamp expireTime = new Timestamp(System.currentTimeMillis() + 1000 * 3600 * 24 * 7); +} diff --git a/src/main/java/com/xclhove/xnote/pojo/form/shareNoteRecord/ShareNoteRecordUpdateForm.java b/src/main/java/com/xclhove/xnote/pojo/form/shareNoteRecord/ShareNoteRecordUpdateForm.java new file mode 100644 index 0000000..6f4c91b --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/form/shareNoteRecord/ShareNoteRecordUpdateForm.java @@ -0,0 +1,22 @@ +package com.xclhove.xnote.pojo.form.shareNoteRecord; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.sql.Timestamp; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class ShareNoteRecordUpdateForm { + private Integer id; + private Timestamp expireTime = new Timestamp(System.currentTimeMillis() + 1000 * 3600 * 24 * 7); +} diff --git a/src/main/java/com/xclhove/xnote/pojo/form/user/UserLoginForm.java b/src/main/java/com/xclhove/xnote/pojo/form/user/UserLoginForm.java new file mode 100644 index 0000000..c0c8358 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/form/user/UserLoginForm.java @@ -0,0 +1,29 @@ +package com.xclhove.xnote.pojo.form.user; + +import com.xclhove.xnote.pojo.form.rule.UserFormRule; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import java.io.Serializable; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class UserLoginForm implements Serializable { + @NotBlank(message = "账号不能为空") + @Pattern(regexp = UserFormRule.Account.REGEXP, message = UserFormRule.Account.MESSAGE) + private String account; + @NotBlank(message = "密码不能为空") + @Pattern(regexp = UserFormRule.Password.REGEXP, message = UserFormRule.Password.MESSAGE) + private String password; +} diff --git a/src/main/java/com/xclhove/xnote/pojo/form/user/UserRegisterForm.java b/src/main/java/com/xclhove/xnote/pojo/form/user/UserRegisterForm.java new file mode 100644 index 0000000..f5726e7 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/form/user/UserRegisterForm.java @@ -0,0 +1,37 @@ +package com.xclhove.xnote.pojo.form.user; + +import com.xclhove.xnote.pojo.form.rule.UserFormRule; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import java.io.Serializable; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class UserRegisterForm implements Serializable { + @NotBlank(message = "用户名不能为空") + @Pattern(regexp = UserFormRule.Name.REGEXP, message = UserFormRule.Name.MESSAGE) + private String name; + @NotBlank(message = "账号不能为空") + @Pattern(regexp = UserFormRule.Account.REGEXP, message = UserFormRule.Account.MESSAGE) + private String account; + @NotBlank(message = "密码不能为空") + @Pattern(regexp = UserFormRule.Password.REGEXP, message = UserFormRule.Password.MESSAGE) + private String password; + @NotBlank(message = "邮箱不能为空") + @Pattern(regexp = UserFormRule.Email.REGEXP, message = UserFormRule.Email.MESSAGE) + private String email; + @NotBlank(message = "验证码不能为空") + private String verificationCode; +} diff --git a/src/main/java/com/xclhove/xnote/pojo/form/user/UserUpdateEmailForm.java b/src/main/java/com/xclhove/xnote/pojo/form/user/UserUpdateEmailForm.java new file mode 100644 index 0000000..a10c001 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/form/user/UserUpdateEmailForm.java @@ -0,0 +1,28 @@ +package com.xclhove.xnote.pojo.form.user; + +import com.xclhove.xnote.pojo.form.rule.UserFormRule; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import java.io.Serializable; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class UserUpdateEmailForm implements Serializable { + @NotBlank(message = "邮箱不能为空") + @Pattern(regexp = UserFormRule.Email.REGEXP, message = UserFormRule.Email.MESSAGE) + private String email; + @NotBlank(message = "验证码不能为空") + private String verificationCode; +} diff --git a/src/main/java/com/xclhove/xnote/pojo/form/user/UserUpdateForm.java b/src/main/java/com/xclhove/xnote/pojo/form/user/UserUpdateForm.java new file mode 100644 index 0000000..61ea773 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/form/user/UserUpdateForm.java @@ -0,0 +1,27 @@ +package com.xclhove.xnote.pojo.form.user; + +import com.xclhove.xnote.pojo.form.rule.UserFormRule; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Positive; +import java.io.Serializable; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class UserUpdateForm implements Serializable { + @Pattern(regexp = UserFormRule.Name.REGEXP, message = UserFormRule.Name.MESSAGE) + private String name; + @Positive(message = "homePageNoteId必须为正整数") + private Integer homePageNoteId; +} diff --git a/src/main/java/com/xclhove/xnote/pojo/form/user/UserUpdatePasswordForm.java b/src/main/java/com/xclhove/xnote/pojo/form/user/UserUpdatePasswordForm.java new file mode 100644 index 0000000..827cd64 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/form/user/UserUpdatePasswordForm.java @@ -0,0 +1,24 @@ +package com.xclhove.xnote.pojo.form.user; + +import com.xclhove.xnote.pojo.form.rule.UserFormRule; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.Pattern; +import java.io.Serializable; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class UserUpdatePasswordForm implements Serializable { + @Pattern(regexp = UserFormRule.Password.REGEXP, message = UserFormRule.Password.MESSAGE) + private String password; +} diff --git a/src/main/java/com/xclhove/xnote/pojo/table/Image.java b/src/main/java/com/xclhove/xnote/pojo/table/Image.java new file mode 100644 index 0000000..e5b3180 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/table/Image.java @@ -0,0 +1,28 @@ +package com.xclhove.xnote.pojo.table; + +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class Image implements Serializable { + @TableId + private Integer id; + private String name; + private Timestamp lastDownloadTime; + private Long size; + private Integer ownerCount; +} diff --git a/src/main/java/com/xclhove/xnote/pojo/table/Note.java b/src/main/java/com/xclhove/xnote/pojo/table/Note.java new file mode 100644 index 0000000..a8764dd --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/table/Note.java @@ -0,0 +1,38 @@ +package com.xclhove.xnote.pojo.table; + +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.annotation.TableId; +import com.xclhove.xnote.pojo.enums.NoteIsPublic; +import com.xclhove.xnote.pojo.es.NoteDoc; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class Note implements Serializable { + @TableId + private Integer id; + private String title; + private String content; + private Integer userId; + private Timestamp releaseTime; + private Timestamp updateTime; + private NoteIsPublic isPublic; + private Integer typeId; + + public NoteDoc toNoteDoc() { + return BeanUtil.copyProperties(this, NoteDoc.class); + } +} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/pojo/table/NoteType.java b/src/main/java/com/xclhove/xnote/pojo/table/NoteType.java new file mode 100644 index 0000000..07ccc17 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/table/NoteType.java @@ -0,0 +1,25 @@ +package com.xclhove.xnote.pojo.table; + +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class NoteType implements Serializable { + @TableId + private Integer id; + private String name; + private Integer userId; +} diff --git a/src/main/java/com/xclhove/xnote/pojo/table/ShareNoteRecord.java b/src/main/java/com/xclhove/xnote/pojo/table/ShareNoteRecord.java new file mode 100644 index 0000000..31dcea9 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/table/ShareNoteRecord.java @@ -0,0 +1,32 @@ +package com.xclhove.xnote.pojo.table; + +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class ShareNoteRecord implements Serializable { + @TableId + private Integer id; + private String code; + private Integer noteId; + private Integer userId; + private Timestamp expireTime; + + public boolean isExpired() { + return expireTime.getTime() < System.currentTimeMillis(); + } +} diff --git a/src/main/java/com/xclhove/xnote/entity/table/Admin.java b/src/main/java/com/xclhove/xnote/pojo/table/User.java similarity index 50% rename from src/main/java/com/xclhove/xnote/entity/table/Admin.java rename to src/main/java/com/xclhove/xnote/pojo/table/User.java index 4dfb707..b540245 100644 --- a/src/main/java/com/xclhove/xnote/entity/table/Admin.java +++ b/src/main/java/com/xclhove/xnote/pojo/table/User.java @@ -1,38 +1,34 @@ -package com.xclhove.xnote.entity.table; +package com.xclhove.xnote.pojo.table; import com.baomidou.mybatisplus.annotation.TableId; import com.fasterxml.jackson.annotation.JsonIgnore; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiParam; +import com.xclhove.xnote.pojo.enums.UserStatus; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; /** - * 管理员 - * * @author xclhove */ @Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) @AllArgsConstructor @NoArgsConstructor -@ApiModel(value = "管理员") -public class Admin { - +public class User implements Serializable { + @TableId - @ApiParam(value = "管理员id") private Integer id; - - @ApiParam(value = "管理员名") private String name; - - @ApiParam(value = "管理员账号") private String account; - @JsonIgnore - @ApiParam(value = "管理员密码") private String password; - - @ApiParam(value = "管理员邮箱") private String email; -} + private UserStatus status; + private Integer homePageNoteId; + private Long imageStorageSize; +} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/pojo/table/UserImage.java b/src/main/java/com/xclhove/xnote/pojo/table/UserImage.java new file mode 100644 index 0000000..b6d884e --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/table/UserImage.java @@ -0,0 +1,26 @@ +package com.xclhove.xnote.pojo.table; + +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class UserImage implements Serializable { + @TableId + private Integer id; + private Integer userId; + private Integer imageId; + private String alias; +} diff --git a/src/main/java/com/xclhove/xnote/pojo/vo/PageVO.java b/src/main/java/com/xclhove/xnote/pojo/vo/PageVO.java new file mode 100644 index 0000000..346f786 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/vo/PageVO.java @@ -0,0 +1,26 @@ +package com.xclhove.xnote.pojo.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.List; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class PageVO implements Serializable { + private Long current; + private Long page; + private Long size; + private Long total; + private List records; +} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/pojo/vo/SearchNoteVO.java b/src/main/java/com/xclhove/xnote/pojo/vo/SearchNoteVO.java new file mode 100644 index 0000000..d59bb65 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/vo/SearchNoteVO.java @@ -0,0 +1,29 @@ +package com.xclhove.xnote.pojo.vo; + +import com.xclhove.xnote.pojo.enums.NoteIsPublic; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class SearchNoteVO implements Serializable { + private Integer id; + private String title; + private Integer userId; + private Timestamp releaseTime; + private NoteIsPublic isPublic; + private Timestamp updateTime; + private Integer typeId; +} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/pojo/vo/SearchUserImageVO.java b/src/main/java/com/xclhove/xnote/pojo/vo/SearchUserImageVO.java new file mode 100644 index 0000000..65e43fc --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/vo/SearchUserImageVO.java @@ -0,0 +1,26 @@ +package com.xclhove.xnote.pojo.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class SearchUserImageVO implements Serializable { + private Integer id; + private Integer userId; + private Integer imageId; + private String alias; + private Timestamp lastDownloadTime; +} diff --git a/src/main/java/com/xclhove/xnote/pojo/vo/ShareNoteRecordVO.java b/src/main/java/com/xclhove/xnote/pojo/vo/ShareNoteRecordVO.java new file mode 100644 index 0000000..a937c18 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/pojo/vo/ShareNoteRecordVO.java @@ -0,0 +1,27 @@ +package com.xclhove.xnote.pojo.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.sql.Date; + +/** + * @author xclhove + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +public class ShareNoteRecordVO implements Serializable { + private Integer id; + private String code; + private Integer noteId; + private Integer userId; + private Date expireTime; + private String title; +} diff --git a/src/main/java/com/xclhove/xnote/repository/NoteRepository.java b/src/main/java/com/xclhove/xnote/repository/NoteRepository.java new file mode 100644 index 0000000..9098171 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/repository/NoteRepository.java @@ -0,0 +1,10 @@ +package com.xclhove.xnote.repository; + +import com.xclhove.xnote.pojo.es.NoteDoc; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +/** + * @author xclhove + */ +public interface NoteRepository extends ElasticsearchRepository { +} diff --git a/src/main/java/com/xclhove/xnote/runner/AbstractRunner.java b/src/main/java/com/xclhove/xnote/runner/AbstractRunner.java deleted file mode 100644 index a0b9833..0000000 --- a/src/main/java/com/xclhove/xnote/runner/AbstractRunner.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.xclhove.xnote.runner; - -import com.xclhove.xnote.config.RunnerConfig; -import com.xclhove.xnote.util.SubclassFinder; -import lombok.Data; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.ApplicationContext; -import org.springframework.stereotype.Component; - -import javax.annotation.Resource; -import java.util.Set; - -/** - * @author xclhove - */ -@Component -@Data -public abstract class AbstractRunner implements ApplicationRunner { - @Resource - private final ApplicationContext applicationContext; - @Resource - protected RunnerConfig runnerConfig; - - @Override - public void run(ApplicationArguments args) { - // 获取AbstractRunner的子类 - Set subclasses = SubclassFinder.findSubclasses(AbstractRunner.class); - - // 执行AbstractRunner子类的doRun方法 - subclasses.forEach(beanDefinition -> { - try { - Class aClass = Class.forName(beanDefinition.getBeanClassName()); - AbstractRunner runner = (AbstractRunner) applicationContext.getBean(aClass); - runner.doRun(args); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - } - - public abstract void doRun(ApplicationArguments args); -} diff --git a/src/main/java/com/xclhove/xnote/runner/ElasticSearchRunner.java b/src/main/java/com/xclhove/xnote/runner/ElasticSearchRunner.java new file mode 100644 index 0000000..8122dcd --- /dev/null +++ b/src/main/java/com/xclhove/xnote/runner/ElasticSearchRunner.java @@ -0,0 +1,59 @@ +package com.xclhove.xnote.runner; + +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.xclhove.xnote.config.XnoteConfig; +import com.xclhove.xnote.pojo.es.NoteDoc; +import com.xclhove.xnote.pojo.table.Note; +import com.xclhove.xnote.repository.NoteRepository; +import com.xclhove.xnote.service.NoteService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 启动时检查能否发送邮件 + * + * @author xclhove + */ +@Component +@Slf4j +@RequiredArgsConstructor +public final class ElasticSearchRunner implements ApplicationRunner { + private final NoteRepository noteRepository; + private final NoteService noteService; + private final XnoteConfig xnoteConfig; + + @Override + public void run(ApplicationArguments args) { + if (!xnoteConfig.runner.getImportDataFromDatabaseToElasticSearch()) { + log.info("已关闭启动时导入数据到 ElasticSearch 的功能"); + return; + } + log.info("开始导入数据到 ElasticSearch..."); + importDataFromDatabaseToElasticSearch(); + log.info("导入数据到 ElasticSearch 成功!"); + } + + private void importDataFromDatabaseToElasticSearch() { + noteRepository.deleteAll(); + + int current = 1; + long pageSize = 1000L; + long total = 0; + + do { + Page page = noteService.page(new Page<>(current, pageSize)); + List noteDocs = BeanUtil.copyToList(page.getRecords(), NoteDoc.class); + //noteDocs.forEach(NoteDoc::generateSuggestionField); + noteRepository.saveAll(noteDocs); + + total = page.getTotal(); + current++; + } while ((current - 1) * pageSize < total); + } +} diff --git a/src/main/java/com/xclhove/xnote/runner/EmailRunner.java b/src/main/java/com/xclhove/xnote/runner/EmailRunner.java new file mode 100644 index 0000000..de882c4 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/runner/EmailRunner.java @@ -0,0 +1,45 @@ +package com.xclhove.xnote.runner; + +import com.xclhove.xnote.config.JavaMailConfig; +import com.xclhove.xnote.config.XnoteConfig; +import com.xclhove.xnote.tool.EmailTool; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +import javax.mail.MessagingException; + +/** + * 启动时检查能否发送邮件 + * + * @author xclhove + */ +@Component +@Slf4j +@RequiredArgsConstructor +public final class EmailRunner implements ApplicationRunner { + private final EmailTool emailTool; + private final JavaMailConfig javaMailConfig; + private final XnoteConfig xnoteConfig; + + @Override + public void run(ApplicationArguments args) { + if (!xnoteConfig.runner.getSendStartupEmail()) { + log.info("不发送启动通知邮件!"); + return; + } + sendStartupEmail(); + } + + private void sendStartupEmail() { + try { + emailTool.sendMail(javaMailConfig.getUsername(), "XNote启动成功!", "XNote启动成功!"); + log.info("启动通知邮件发送成功!"); + } catch (MessagingException e) { + log.error("启动通知邮件发送失败", e); + System.exit(1); + } + } +} diff --git a/src/main/java/com/xclhove/xnote/runner/ImageRunner.java b/src/main/java/com/xclhove/xnote/runner/ImageRunner.java new file mode 100644 index 0000000..226a937 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/runner/ImageRunner.java @@ -0,0 +1,73 @@ +package com.xclhove.xnote.runner; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.xclhove.xnote.config.XnoteConfig; +import com.xclhove.xnote.pojo.table.Image; +import com.xclhove.xnote.service.ImageService; +import com.xclhove.xnote.tool.MinioTool; +import io.minio.StatObjectResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author xclhove + */ +@Component +@Slf4j +@RequiredArgsConstructor +@Order +public final class ImageRunner implements ApplicationRunner { + private final XnoteConfig xnoteConfig; + private final MinioTool minioTool; + private final ImageService imageService; + + @Override + public void run(ApplicationArguments args) { + if (!xnoteConfig.runner.getUpdateImageSize()) { + log.info("已关闭启动时更新图片大小"); + } else { + log.info("开始更新图片大小..."); + updateImageSize(); + log.info("更新图片大小完成!"); + } + } + + private void updateImageSize() { + long total = 0; + long current = 1; + long pageSize = 1000L; + do { + Page page = imageService.page(new Page<>(current, pageSize)); + List deleteImages = new ArrayList<>(); + List updateImages = page + .getRecords() + .stream() + .filter(image -> image.getSize() != null) + .filter(image -> { + try { + StatObjectResponse file = minioTool.getFile(image.getName()); + if (file == null) { + deleteImages.add(image); + return false; + } + image.setSize(file.size()); + } catch (Exception e) { + log.error("获取图片 {} 大小失败", image.getName(), e); + } + return true; + }).collect(Collectors.toList()); + imageService.updateBatchByIdWithRedis(updateImages); + imageService.removeBatchByIdsWithRedis(deleteImages); + total = page.getTotal(); + current++; + } while ((current - 1) * pageSize < total); + } +} diff --git a/src/main/java/com/xclhove/xnote/runner/MinioRunner.java b/src/main/java/com/xclhove/xnote/runner/MinioRunner.java index b91d465..60bfe4f 100644 --- a/src/main/java/com/xclhove/xnote/runner/MinioRunner.java +++ b/src/main/java/com/xclhove/xnote/runner/MinioRunner.java @@ -1,13 +1,13 @@ package com.xclhove.xnote.runner; +import com.xclhove.xnote.config.XnoteConfig; import com.xclhove.xnote.tool.MinioTool; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.ApplicationArguments; -import org.springframework.context.ApplicationContext; +import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; -import javax.annotation.Resource; - /** * 启动时检查minio是否连接成功 * @@ -15,16 +15,13 @@ */ @Component @Slf4j -public class MinioRunner extends AbstractRunner { - @Resource - private MinioTool minioTool; - - public MinioRunner(ApplicationContext applicationContext) { - super(applicationContext); - } +@RequiredArgsConstructor +public class MinioRunner implements ApplicationRunner { + private final MinioTool minioTool; + private final XnoteConfig xnoteConfig; @Override - public void doRun(ApplicationArguments args) { + public void run(ApplicationArguments args) { checkMinio(); } @@ -32,19 +29,20 @@ public void doRun(ApplicationArguments args) { * 检查minio是否连接成功 */ private void checkMinio() { - if (!runnerConfig.getEnableMinioStatusCheckRunner()) { + if (!xnoteConfig.runner.getEnableCheckMinioStatus()) { + log.info("跳过检查 minio 服务状态"); return; } try { boolean bucketExist = minioTool.bucketExist(); if (bucketExist) { - log.info("minio连接成功!"); + log.info("minio 连接成功!"); return; } log.info("minio存储桶未创建,创建存储桶……"); - bucketExist = minioTool.creatBucket(); + bucketExist = minioTool.createBucket(); if (!bucketExist) { log.error("创建存储桶失败!"); diff --git a/src/main/java/com/xclhove/xnote/runner/NoteRunner.java b/src/main/java/com/xclhove/xnote/runner/NoteRunner.java index 5fe8763..75cd161 100644 --- a/src/main/java/com/xclhove/xnote/runner/NoteRunner.java +++ b/src/main/java/com/xclhove/xnote/runner/NoteRunner.java @@ -1,100 +1,99 @@ package com.xclhove.xnote.runner; -import com.alibaba.fastjson2.JSON; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.xclhove.xnote.entity.attribute.NoteKeyword; -import com.xclhove.xnote.entity.table.Note; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.xclhove.xnote.config.XnoteConfig; +import com.xclhove.xnote.pojo.table.Note; +import com.xclhove.xnote.pojo.table.NoteType; +import com.xclhove.xnote.pojo.table.User; import com.xclhove.xnote.service.NoteService; +import com.xclhove.xnote.service.NoteTypeService; +import com.xclhove.xnote.service.UserService; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.ApplicationArguments; -import org.springframework.context.ApplicationContext; +import org.springframework.boot.ApplicationRunner; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; -import javax.annotation.Resource; -import java.util.ArrayList; -import java.util.List; +import java.util.HashSet; +import java.util.Set; /** - * Xnote启动初始化类 - * * @author xclhove */ @Component @Slf4j +@RequiredArgsConstructor @Order -public class NoteRunner extends AbstractRunner { - @Resource - private NoteService noteService; - - public NoteRunner(ApplicationContext applicationContext) { - super(applicationContext); - } +public final class NoteRunner implements ApplicationRunner { + private final XnoteConfig xnoteConfig; + private final NoteTypeService noteTypeService; + private final UserService userService; + private final NoteService noteService; @Override - public void doRun(ApplicationArguments args) { - convertKeywords(); - replaceImageUrl(); - } - - /** - * 转换关键词的存储结构 - */ - private void convertKeywords() { - if (!runnerConfig.getEnableKeywordsConvertRunner()) { - return; + public void run(ApplicationArguments args) { + if (!xnoteConfig.runner.getCreateDefaultNoteTypeForUserOfNoneType()) { + log.info("已关闭启动时为用户创建默认笔记类型"); + } else { + log.info("开始为用户创建默认笔记类型..."); + createDefaultNoteTypeForUserOfNoneType(); + log.info("为用户创建默认笔记类型完成!"); } - try { - log.info("关键词转化……"); - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.select(Note::getId, Note::getKeywords); - List notes = noteService.list(queryWrapper); - List newNotes = new ArrayList<>(); - - for (Note note : notes) { - List keywords = JSON.parseArray(JSON.toJSONString(note.getKeywords()), NoteKeyword.class); - - List newKeywords = new ArrayList<>(); - keywords.forEach(noteKeyword -> { - newKeywords.add(noteKeyword.getName()); - }); - - Note newNote = new Note(); - newNote.setKeywords(newKeywords); - newNote.setId(note.getId()); - - newNotes.add(newNote); - - if (newNotes.size() == 500) { - noteService.updateBatchById(newNotes); - newNotes.clear(); - } - } - - noteService.updateBatchById(newNotes); - - log.info("关键词转化已经完成!"); - System.exit(0); - } catch (Exception e) { - log.info("关键词无需转化!"); + if (!xnoteConfig.runner.getReplaceImageUrlInNote()) { + log.info("已关闭启动时替换笔记中的图片url"); + } else { + log.info("开始替换笔记中的图片url..."); + replaceImageUrlInNote(); + log.info("替换笔记中的图片 url 完成!"); } } - /** - * 替换笔记中的图片地址 - */ - private void replaceImageUrl() { - if (!runnerConfig.getEnableImageUrlReplaceRunner()) { - return; - } + private void createDefaultNoteTypeForUserOfNoneType() { + long total = 0; + long current = 1; + long pageSize = 1000L; + + LambdaQueryWrapper noteTypeQueryWrapper = new LambdaQueryWrapper<>(); + noteTypeQueryWrapper.select(NoteType::getUserId); + Set userOfHasNoteTypeIds = new HashSet<>(); + noteTypeService.page(new Page<>(current, pageSize), noteTypeQueryWrapper) + .getRecords() + .forEach(noteType -> userOfHasNoteTypeIds.add(noteType.getUserId())); + + do { + Set userOfNoneNoteTypeIds = new HashSet<>(); + LambdaQueryWrapper userLambdaQueryWrapper = new LambdaQueryWrapper<>(); + userLambdaQueryWrapper.select(User::getId); + userLambdaQueryWrapper.notIn(!userOfHasNoteTypeIds.isEmpty(), User::getId, userOfHasNoteTypeIds); + Page page = userService.page(new Page<>(current, pageSize), userLambdaQueryWrapper); + page.getRecords().forEach(user -> userOfNoneNoteTypeIds.add(user.getId())); + userOfNoneNoteTypeIds.forEach(noteTypeService::createDefaultAndSetToNoteOfNoneType); + total = page.getTotal(); + current++; + } while ((current - 1) * pageSize < total); + } + + private void replaceImageUrlInNote() { + long total = 0; + long current = 1; + long pageSize = 1000L; - List list = new ArrayList<>(); - noteService.list().forEach(note -> { - note.setContent(note.getContent().replaceAll("xnote.xclhove.top/api", "api.xclhove.top/xnote")); - list.add(note); - }); - noteService.updateBatchById(list); - log.info("替换图片地址成功!"); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + do { + Page page = noteService.page(new Page<>(current, pageSize), queryWrapper); + page.getRecords().forEach(note -> note.setContent(note.getContent() + .replaceAll( + "https://api.xclhove.top/xnote/image/downloadByName", + "/api/image/name" + ) + )); + noteService.updateBatchByIdWithRedisAndEs(page.getRecords()); + + total = page.getTotal(); + current++; + } while ((current - 1) * pageSize < total); } } diff --git a/src/main/java/com/xclhove/xnote/runner/RedisRunner.java b/src/main/java/com/xclhove/xnote/runner/RedisRunner.java index dd2af93..e679c83 100644 --- a/src/main/java/com/xclhove/xnote/runner/RedisRunner.java +++ b/src/main/java/com/xclhove/xnote/runner/RedisRunner.java @@ -1,13 +1,16 @@ package com.xclhove.xnote.runner; -import com.xclhove.xnote.tool.RedisTool; +import com.xclhove.xnote.config.XnoteConfig; +import com.xclhove.xnote.constant.RedisKey; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.ApplicationArguments; -import org.springframework.context.ApplicationContext; -import org.springframework.core.annotation.Order; +import org.springframework.boot.ApplicationRunner; +import org.springframework.data.redis.RedisConnectionFailureException; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; -import javax.annotation.Resource; +import java.util.concurrent.TimeUnit; /** * 启动时检查redis是否连接成功 @@ -16,17 +19,13 @@ */ @Component @Slf4j -@Order(1) -public class RedisRunner extends AbstractRunner { - @Resource - private RedisTool redisTool; - - public RedisRunner(ApplicationContext applicationContext) { - super(applicationContext); - } +@RequiredArgsConstructor +public class RedisRunner implements ApplicationRunner { + private final StringRedisTemplate stringRedisTemplate; + private final XnoteConfig xnoteConfig; @Override - public void doRun(ApplicationArguments args) { + public void run(ApplicationArguments args) { checkRedisStatus(); } @@ -34,15 +33,26 @@ public void doRun(ApplicationArguments args) { * 检查redis是否连接成功 */ private void checkRedisStatus() { - if (!runnerConfig.getEnableRedisStatusCheckRunner()) { + if (!xnoteConfig.runner.getEnableCheckRedisStatus()) { + log.info("跳过检查 redis 服务状态"); return; } - if (!redisTool.connected()) { - log.error("redis连接失败!请检查配置文件和redis是否启动。"); + if (!connected()) { + log.error("redis 连接失败!请检查配置文件和redis是否启动。"); System.exit(1); return; } - log.info("redis连接成功!"); + log.info("redis 连接成功!"); + } + + private boolean connected() { + try { + String redisKey = RedisKey.join(RedisKey.PROJECT, RedisKey.REDIS_CONNECT_TEST); + stringRedisTemplate.opsForValue().set(redisKey, "test", 1, TimeUnit.SECONDS); + return true; + } catch (RedisConnectionFailureException e) { + return false; + } } } diff --git a/src/main/java/com/xclhove/xnote/service/AdminService.java b/src/main/java/com/xclhove/xnote/service/AdminService.java deleted file mode 100644 index 3842333..0000000 --- a/src/main/java/com/xclhove/xnote/service/AdminService.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.xclhove.xnote.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.xclhove.xnote.entity.table.Admin; - -import java.util.List; - -/** - * @author xclhove - */ -public interface AdminService extends IService { - /** - * 添加管理员 - * - * @param admin 管理员信息 - * @return 已添加的管理员信息 - */ - public boolean addAdmin(Admin admin); - - /** - * 删除管理员 - * - * @param id 管理员id - * @return 已删除的管理员信息 - */ - public boolean deleteAdmin(Integer id); - - /** - * 修改管理员信息 - * - * @param admin 管理员信息 - * @return 已修改的管理员信息 - */ - public boolean changeAdmin(Admin admin); - - /** - * 获取管理员信息 - * - * @param id 管理员id - * @return 管理员信息 - */ - public Admin getAdmin(Integer id); - - /** - * 分页获取所有管理员信息 - * - * @param pageNumber 页码 - * @param pageSize 每页大小 - * @return 管理员信息集合 - */ - public List getAdmins(Integer pageNumber, Integer pageSize); -} diff --git a/src/main/java/com/xclhove/xnote/service/ImageService.java b/src/main/java/com/xclhove/xnote/service/ImageService.java index e0837c4..ff5328b 100644 --- a/src/main/java/com/xclhove/xnote/service/ImageService.java +++ b/src/main/java/com/xclhove/xnote/service/ImageService.java @@ -1,87 +1,248 @@ package com.xclhove.xnote.service; -import com.baomidou.mybatisplus.extension.service.IService; -import com.xclhove.xnote.entity.dto.ImagePageDTO; -import com.xclhove.xnote.entity.table.Image; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xclhove.xnote.config.XnoteConfig; +import com.xclhove.xnote.constant.RedisKey; +import com.xclhove.xnote.exception.ImageServiceException; +import com.xclhove.xnote.mapper.ImageMapper; +import com.xclhove.xnote.pojo.table.Image; +import com.xclhove.xnote.pojo.table.User; +import com.xclhove.xnote.pojo.table.UserImage; +import com.xclhove.xnote.tool.MinioTool; +import com.xclhove.xnote.tool.RedisTool; +import com.xclhove.xnote.util.ByteSizeUtil; +import com.xclhove.xnote.util.Md5Util; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; -import javax.servlet.http.HttpServletResponse; -import java.util.List; +import javax.annotation.Resource; +import java.sql.Timestamp; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; /** - *

- * 图片表 服务类 - *

- * * @author xclhove - * @since 2023-12-09 */ -public interface ImageService extends IService { +@Service +@RequiredArgsConstructor +public class ImageService extends ServiceImpl { + private final static int IMAGE_ALIAS_MAX_LENGTH = 30; + private final static String IMAGE_CONTENT_TYPE = "^image/(png|jpg|jpeg|svg|ico|gif|bmp)+$"; - /** - * 上传图片 - * - * @param userId 用户id - * @param imageFile 图片文件流 - * @return 上传后的图片信息 - */ - Image upload(Integer userId, MultipartFile imageFile); + private final MinioTool minioTool; + private final StringRedisTemplate stringRedisTemplate; + private final XnoteConfig xnoteConfig; + private final RedisTool redisTool; + private final ImageMapper imageMapper; - /** - * 批量删除图片 - * - * @param userId 用户id,普通用户删除图片时必须传入,管理员删除图片时无需传入 - * @param imageIds 图片id列表 - * @return 删除成功返回true,否则返回false。 - */ - boolean deleteByIds(Integer userId, List imageIds); + @Resource + private UserImageService userImageService; /** - * 获取图片信息 - * - * @param userId 用户id,普通用户删除图片时必须传入,管理员删除图片时无需传入 - * @param imageId 图片id - * @return 返回图片信息 + * 防缓存穿透 */ - Image get(Integer userId, Integer imageId); + private Image getWithRedis(String redisKey, RedisTool.Getter getter) { + return redisTool.getUseStringAntiCachePassThrough( + redisKey, + getter, + JSON::toJSONString, + jsonString -> JSON.parseObject(jsonString, Image.class), + 30, + TimeUnit.MINUTES + ); + } - /** - * 分页获取image,设置了userId则获取单个用户的,不设置则获取所有用户的 - * - * @param pageDTO 分页信息 - * @return 返回分页后的图片信息列表 - */ - ImagePageDTO page(ImagePageDTO pageDTO); + public String getImageRedisKeyByName(String imageName) { + return RedisKey.join(RedisKey.Image.NAME, imageName); + } - /** - * 修改图片信息 - * - * @param image 图片信息,设置了userId则为普通用户修改自己的图片,不设置则为管理员修改任意图片 - * @return 修改成功返回true,否则返回false。 - */ - boolean change(Image image); + public String getImageUrlRedisKeyByName(String imageName) { + return RedisKey.join(RedisKey.Image.URL, imageName); + } - /** - * 下载图片 - * - * @param response 响应对象 - * @param userId 用户id - * @param imageId 图片id - */ - void downloadById(HttpServletResponse response, Integer userId, Integer imageId); + public String getUserImageTotalSizeRedisKey(int userId) { + return RedisKey.join(RedisKey.Image.USER_TOTAL_SIZE, String.valueOf(userId)); + } - /** - * 下载图片 - * - * @param response 响应对象 - * @param imageName 图片名称 - */ - void downloadByName(HttpServletResponse response, String imageName); + public long countUserImageTotalSize(int userId) { + List imageIds = userImageService.listByUserId(userId) + .stream() + .map(UserImage::getImageId) + .collect(Collectors.toList()); + if (imageIds.isEmpty()) { + return 0; + } + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.select(Image::getSize); + queryWrapper.in(Image::getId, imageIds); + List images = this.list(queryWrapper); + long totalSize = images.stream().mapToLong(Image::getSize).sum(); + return totalSize; + } + + public long countUserImageTotalSizeWithRedis(int userId) { + return redisTool.getUseString( + getUserImageTotalSizeRedisKey(userId), + () -> countUserImageTotalSize(userId), + String::valueOf, + Long::parseLong, + 30, + TimeUnit.MINUTES + ); + } + + public String upload(User user, MultipartFile file) { + long imageSize = file.getSize(); + final Long allowMaxSizeOfByte = xnoteConfig.image.getAllowSizeOfByte(); + if (imageSize > allowMaxSizeOfByte) { + throw new ImageServiceException("图片大小不能超过" + ByteSizeUtil.parseSizeWithUnit(allowMaxSizeOfByte)); + } + + String contentType = file.getContentType(); + if (StrUtil.isBlank(contentType) || !contentType.matches(IMAGE_CONTENT_TYPE)) { + throw new ImageServiceException("图片格式错误"); + } + + StringBuilder imageName = new StringBuilder(); + imageName.append(user.getId()); + imageName.append("_"); + try { + imageName.append(Md5Util.getMd5(file.getBytes())); + } catch (Exception e) { + throw new ImageServiceException("系统异常,图片上传失败"); + } + imageName.append("."); + imageName.append(contentType.substring(contentType.lastIndexOf("/") + 1)); + + // 最后需要清除防缓存穿透产生的空数据 + Image existedImage = getByNameWithRedis(imageName.toString()); + if (existedImage != null) { + if (userImageService.getByUserAndImageIdWithRedis(user.getId(), existedImage.getId()) != null) { + return imageName.toString(); + } + + UserImage userImage = new UserImage(); + userImage.setImageId(existedImage.getId()); + userImage.setUserId(user.getId()); + userImage.setAlias(file.getOriginalFilename()); + boolean saveSuccess = userImageService.saveWithRedis(userImage); + if (!saveSuccess) { + throw new ImageServiceException("图片上传失败"); + } + imageMapper.incrementImageOwnerCount(Collections.singletonList(existedImage.getId()), 1); + return imageName.toString(); + } + + long totalSize = countUserImageTotalSizeWithRedis(user.getId()) + imageSize; + if (totalSize > user.getImageStorageSize()) { + throw new ImageServiceException("图片存储空间不足"); + } + + try { + minioTool.upload(file, imageName.toString()); + } catch (Exception e) { + log.error(e.getMessage(), e); + throw new ImageServiceException("图片上传失败"); + } + + Image image = new Image(); + image.setName(imageName.toString()); + image.setSize(imageSize); + boolean saveSuccess = this.save(image); + if (!saveSuccess) { + try { + minioTool.deleteFile(imageName.toString()); + } catch (Exception e) { + log.error("删除 minio 中的图片失败", e); + } + throw new ImageServiceException("图片信息保存失败"); + } + + UserImage userImage = new UserImage(); + userImage.setImageId(image.getId()); + userImage.setUserId(user.getId()); + userImage.setAlias(file.getOriginalFilename()); + boolean userImageSaveSuccess = userImageService.save(userImage); + if (!userImageSaveSuccess) { + throw new ImageServiceException("用户图片上传失败"); + } + + // 更新 redis 中的用户图片总大小 + stringRedisTemplate.opsForValue().increment(getUserImageTotalSizeRedisKey(user.getId()), imageSize); + // 清除防缓存穿透的空数据 + stringRedisTemplate.delete(getImageRedisKeyByName(imageName.toString())); + return imageName.toString(); + } + + @Nullable + public Image getByName(String imageName) { + Image image = getOne(new LambdaQueryWrapper().eq(Image::getName, imageName)); + if (image == null) { + return null; + } + image.setLastDownloadTime(new Timestamp(System.currentTimeMillis())); + updateById(image); + return image; + } /** - * 下载图片 - * @param response 响应对象 - * @param image 图片信息 + * 防缓存穿透 */ - void download(HttpServletResponse response, Image image); + @Nullable + public Image getByNameWithRedis(String imageName) { + return getWithRedis(getImageRedisKeyByName(imageName), () -> getByName(imageName)); + } + + @Nullable + public String getImageUrlByName(String imageName) { + Image image = getByNameWithRedis(imageName); + if (image == null) { + return null; + } + return minioTool.getFileUrl(image.getName()); + } + + @Nullable + public String getImageUrlByNameWithRedis(String imageName) { + return redisTool.getUseStringAntiCachePassThrough( + getImageUrlRedisKeyByName(imageName), + () -> getImageUrlByName(imageName), + (value) -> value, + (value) -> value, + 30, + TimeUnit.MINUTES + ); + } + + public boolean updateBatchByIdWithRedis(List images) { + boolean updateSuccess = this.updateBatchById(images); + if (updateSuccess) { + stringRedisTemplate.delete(images.stream() + .map(image -> getImageRedisKeyByName(image.getName())) + .collect(Collectors.toList()) + ); + } + return updateSuccess; + } + + public boolean removeBatchByIdsWithRedis(Collection images) { + boolean removeSuccess = removeBatchByIds(images); + if (removeSuccess) { + Set keys = new HashSet<>(); + images.forEach(image -> { + keys.add(getImageRedisKeyByName(image.getName())); + keys.add(getImageUrlRedisKeyByName(image.getName())); + }); + stringRedisTemplate.delete(keys); + } + return removeSuccess; + } } diff --git a/src/main/java/com/xclhove/xnote/service/NoteService.java b/src/main/java/com/xclhove/xnote/service/NoteService.java index 4d24713..44f8821 100644 --- a/src/main/java/com/xclhove/xnote/service/NoteService.java +++ b/src/main/java/com/xclhove/xnote/service/NoteService.java @@ -1,58 +1,318 @@ package com.xclhove.xnote.service; -import com.baomidou.mybatisplus.extension.service.IService; -import com.xclhove.xnote.entity.dto.NotePageDTO; -import com.xclhove.xnote.entity.table.Note; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import cn.hutool.core.text.StrBuilder; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xclhove.xnote.config.XnoteConfig; +import com.xclhove.xnote.constant.EsConstant; +import com.xclhove.xnote.constant.RedisKey; +import com.xclhove.xnote.exception.NoteServiceException; +import com.xclhove.xnote.mapper.NoteMapper; +import com.xclhove.xnote.pojo.enums.NoteIsPublic; +import com.xclhove.xnote.pojo.es.NoteDoc; +import com.xclhove.xnote.pojo.form.note.NoteAddForm; +import com.xclhove.xnote.pojo.form.note.NoteUpdateForm; +import com.xclhove.xnote.pojo.form.note.NoteUpdateTypeForm; +import com.xclhove.xnote.pojo.table.Note; +import com.xclhove.xnote.pojo.table.NoteType; +import com.xclhove.xnote.pojo.table.ShareNoteRecord; +import com.xclhove.xnote.pojo.table.User; +import com.xclhove.xnote.pojo.vo.PageVO; +import com.xclhove.xnote.repository.NoteRepository; +import com.xclhove.xnote.tool.ElasticsearchTool; +import com.xclhove.xnote.tool.RedisTool; +import lombok.RequiredArgsConstructor; +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHit; +import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.query.NativeSearchQuery; +import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.sql.Timestamp; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; /** * @author xclhove */ -public interface NoteService extends IService { - /** - * 获取笔记 - * @param userId 用户id - * @param noteId 笔记id - * @param accessCode 访问码 - * @return Note对象 - */ - Note getNoteById(Integer userId, Integer noteId, String accessCode); +@Service +@RequiredArgsConstructor +public class NoteService extends ServiceImpl { + private final StringRedisTemplate stringRedisTemplate; + private final RedisTool redisTool; + private final XnoteConfig xnoteConfig; + private final NoteRepository noteRepository; + private final ElasticsearchOperations elasticsearchOperations; - /** - * 添加笔记 - * @param note 笔记信息 - * @return Note对象(不包含笔记内容) - */ - Note addNote(Note note); + @Resource + private NoteTypeService noteTypeService; + @Resource + private ShareNoteRecordService shareNoteRecordService; - /** - * 更新笔记 - * @param note 笔记信息 - * @return Note对象(不包含笔记内容) - */ - Note updateNote(Note note); + public Note getWithRedis(String redisKey, RedisTool.Getter getter) { + return redisTool.getUseStringAntiCachePassThrough( + redisKey, + getter, + JSON::toJSONString, + jsonString -> JSON.parseObject(jsonString, Note.class), + 30, + TimeUnit.MINUTES + ); + } /** - * 删除笔记 - * @param userId 用户id(用于校验),如果为null,则不校验用户id,直接删除笔记 - * @param noteId 笔记id - * @return 是否删除成功 + * redisKey */ - boolean deleteNote(Integer userId, Integer noteId); + public String getRedisKeyById(int id) { + return RedisKey.join(RedisKey.Note.PREFIX, String.valueOf(id)); + } /** - * 分页获取所有的笔记 - * @param notePageDTO 分页数据传输对象 - * @param requestUserId 请求用户的id,为null则未登录 - * @param isGetSelf 是否获取自己的笔记 - * @return Note对象列表(不包含笔记内容) + * (使用redis做缓存,有防缓存穿透) */ - NotePageDTO pageNote(NotePageDTO notePageDTO, Integer requestUserId, boolean isGetSelf); + public Note getByIdWithRedis(int id) { + return getWithRedis(getRedisKeyById(id), () -> getById(id)); + } - /** - * 分页获取所有的笔记 - * @param notePageDTO 分页数据传输对象 - * @param requestUserId 请求用户的id,为null则未登录 - * @return Note对象列表(不包含笔记内容) - */ - NotePageDTO pageNote(NotePageDTO notePageDTO, Integer requestUserId); -} \ No newline at end of file + public int add(User noteOwner, NoteAddForm noteAddForm) { + NoteType noteType = noteTypeService.getByIdWithRedis(noteAddForm.getTypeId()); + if (noteType == null || !Objects.equals(noteType.getUserId(), noteOwner.getId())) { + noteAddForm.setTypeId(null); + } + + Note note = BeanUtil.copyProperties(noteAddForm, Note.class); + note.setUserId(noteOwner.getId()); + + boolean saveSuccess = this.save(note); + if (!saveSuccess) { + throw new NoteServiceException(); + } + + noteRepository.save(note.toNoteDoc()); + + return note.getId(); + } + + public Note previewNote(@Nullable User user, int targetNoteId, @Nullable String shareCode) { + Note note = getByIdWithRedis(targetNoteId); + if (note == null) { + throw new NoteServiceException("笔记不存在"); + } + if (note.getIsPublic() == NoteIsPublic.YES) { + return note; + } + if (user != null && user.getId().equals(note.getUserId())) { + return note; + } + if (StrUtil.isBlank(shareCode)) { + throw new NoteServiceException("权限不足,无法查看"); + } + ShareNoteRecord shareNoteRecord = shareNoteRecordService.getByCodeWithRedis(shareCode); + if (shareNoteRecord == null || !Objects.equals(shareNoteRecord.getNoteId(), note.getId())) { + throw new NoteServiceException("分享码错误或已失效"); + } + if (shareNoteRecord.isExpired()) { + shareNoteRecordService.deleteById(shareNoteRecord); + throw new NoteServiceException("分享码已失效"); + } + + return note; + } + + public PageVO search(Page page, + @Nullable User user, + @Nullable String search, + @Nullable String heightLightPreTag, + @Nullable String heightLightPostTag + ) { + BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); + if (StrUtil.isNotBlank(search)) { + boolQuery.must(QueryBuilders.matchQuery(EsConstant.NOTE_QUERY_FIELD, search)); + } + + BoolQueryBuilder nestedBoolQuery = QueryBuilders.boolQuery(); + nestedBoolQuery.should(QueryBuilders.termQuery("isPublic", "YES")); + if (user != null) { + nestedBoolQuery.should(QueryBuilders.termQuery("userId", user.getId())); + } + boolQuery.must(nestedBoolQuery); + + NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); + if (StrUtil.isNotBlank(heightLightPreTag) && StrUtil.isNotBlank(heightLightPostTag)) { + queryBuilder.withHighlightBuilder(new HighlightBuilder() + .field(EsConstant.NOTE_QUERY_FIELD) + .preTags(heightLightPreTag) + .postTags(heightLightPostTag) + ); + } + queryBuilder.withQuery(boolQuery); + queryBuilder.withPageable(PageRequest.of(Math.toIntExact(page.getCurrent()) - 1, Math.toIntExact(page.getSize()))); + + NativeSearchQuery searchQuery = queryBuilder.build(); + + SearchHits searchHits = elasticsearchOperations.search(searchQuery, NoteDoc.class); + + List notes = searchHits.getSearchHits() + .stream() + .map(searchHit -> { + NoteDoc noteDoc = searchHit.getContent(); + List heightList = searchHit.getHighlightFields().get(EsConstant.NOTE_QUERY_FIELD); + final int maxLength = xnoteConfig.search.getHeightLightContentMaxLength(); + if (heightList == null || heightList.isEmpty()) { + Note note = noteDoc.toNote(); + String content = note.getContent(); + return note.setContent(content.substring(0, Math.min(content.length(), maxLength))); + } + + StrBuilder content = new StrBuilder(); + heightList.forEach(heightString -> { + int length = content.length(); + if (length > maxLength) { + return; + } + content.append(heightString); + content.append("......"); + }); + noteDoc.setContent(content.toString()); + return noteDoc.toNote(); + }) + .collect(Collectors.toList()); + + PageVO pageVO = BeanUtil.copyProperties(page, PageVO.class); + pageVO.setRecords(notes); + pageVO.setTotal(searchHits.getTotalHits()); + return pageVO; + } + + public void update(User noteOwner, NoteUpdateForm noteUpdateForm) { + Note note = getByIdWithRedis(noteUpdateForm.getId()); + if (note == null) { + throw new NoteServiceException("笔记不存在"); + } + if(!Objects.equals(note.getUserId(), noteOwner.getId())) { + throw new NoteServiceException("权限不足,无法修改"); + } + + + Integer typeId = noteUpdateForm.getTypeId(); + NoteType noteType = null; + if (typeId != null) { + noteType = noteTypeService.getByIdWithRedis(typeId); + } + if (noteType == null || !Objects.equals(noteType.getUserId(), noteOwner.getId())) { + noteUpdateForm.setTypeId(null); + } + + BeanUtil.copyProperties(noteUpdateForm, note, CopyOptions.create().setIgnoreNullValue(true)); + note.setUpdateTime(new Timestamp(System.currentTimeMillis())); + boolean updateSuccess = this.updateById(note); + if (!updateSuccess) { + throw new NoteServiceException("系统异常,保存失败"); + } + + stringRedisTemplate.delete(getRedisKeyById(note.getId())); + noteRepository.save(note.toNoteDoc()); + } + + public void deleteBatchByIds(User noteOwner, List noteIds) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.select(Note::getId); + queryWrapper.eq(Note::getUserId, noteOwner.getId()); + queryWrapper.in(Note::getId, noteIds); + + List deleteNotes = this.list(queryWrapper); + boolean removeSuccess = this.remove(queryWrapper); + if (!removeSuccess) { + throw new NoteServiceException("删除失败"); + } + + // 删除 redis 中的缓存 + Set redisKeys = new HashSet<>(); + deleteNotes.forEach(note -> redisKeys.add(getRedisKeyById(note.getId()))); + stringRedisTemplate.delete(redisKeys); + + noteRepository.deleteAll(BeanUtil.copyToList(deleteNotes, NoteDoc.class)); + } + + public Page searchUserNote(Page page, + @Nullable User user, + @NonNull Integer targetUserId, + @Nullable Integer typeId, + @Nullable String search + ) { + BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); + + boolQuery.must(QueryBuilders.termQuery("userId", targetUserId)); + if (user == null || !user.getId().equals(targetUserId)) { + boolQuery.must(QueryBuilders.termQuery("isPublic", NoteIsPublic.YES)); + } + + if (typeId != null) { + boolQuery.must(QueryBuilders.termQuery("typeId", typeId)); + } + if (StrUtil.isNotBlank(search)) { + boolQuery.must(QueryBuilders.matchQuery(EsConstant.NOTE_QUERY_FIELD, search)); + } + + NativeSearchQuery searchQuery = new NativeSearchQueryBuilder() + .withQuery(boolQuery) + .withPageable(PageRequest.of(Math.toIntExact(page.getCurrent()) - 1, Math.toIntExact(page.getSize()))) + .build(); + + SearchHits searchHits = elasticsearchOperations.search(searchQuery, NoteDoc.class); + + List notes = searchHits.getSearchHits() + .stream() + .map(SearchHit::getContent) + .map(NoteDoc::toNote) + .collect(Collectors.toList()); + + page.setRecords(notes); + page.setTotal(searchHits.getTotalHits()); + return page; + } + + public void updateBatchUserNoteType(User noteOwner, NoteUpdateTypeForm noteUpdateTypeForm) { + NoteType noteType = noteTypeService.getByIdWithRedis(noteUpdateTypeForm.getTypeId()); + if (noteType == null || !Objects.equals(noteType.getUserId(), noteOwner.getId())) { + throw new NoteServiceException("类型不存在"); + } + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(Note::getUserId, noteOwner.getId()); + queryWrapper.in(Note::getId, noteUpdateTypeForm.getNoteIds()); + List updateNotes = this.list(queryWrapper); + + updateNotes.forEach(note -> note.setTypeId(noteType.getId())); + updateBatchByIdWithRedisAndEs(updateNotes); + } + + public void updateBatchByIdWithRedisAndEs(Collection updateNotes) { + boolean updateSuccess = this.updateBatchById(updateNotes); + if (!updateSuccess) { + throw new NoteServiceException("系统异常,修改失败"); + } + + // 删除 redis 中的缓存 + Set redisKeys = new HashSet<>(); + updateNotes.forEach(note -> redisKeys.add(getRedisKeyById(note.getId()))); + stringRedisTemplate.delete(redisKeys); + + noteRepository.saveAll(BeanUtil.copyToList(updateNotes, NoteDoc.class)); + } +} diff --git a/src/main/java/com/xclhove/xnote/service/NoteTypeService.java b/src/main/java/com/xclhove/xnote/service/NoteTypeService.java new file mode 100644 index 0000000..4713079 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/service/NoteTypeService.java @@ -0,0 +1,173 @@ +package com.xclhove.xnote.service; + +import cn.hutool.core.bean.BeanUtil; +import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xclhove.xnote.constant.RedisKey; +import com.xclhove.xnote.exception.NoteTypeServiceException; +import com.xclhove.xnote.mapper.NoteTypeMapper; +import com.xclhove.xnote.pojo.form.noteType.NoteTypeAddForm; +import com.xclhove.xnote.pojo.form.noteType.NoteTypeUpdateForm; +import com.xclhove.xnote.pojo.table.Note; +import com.xclhove.xnote.pojo.table.NoteType; +import com.xclhove.xnote.pojo.table.User; +import com.xclhove.xnote.tool.RedisTool; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * @author xclhove + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class NoteTypeService extends ServiceImpl { + private final StringRedisTemplate stringRedisTemplate; + private final RedisTool redisTool; + @Autowired + private NoteService noteService; + + public NoteType getWithRedis(String redisKey, RedisTool.Getter getter) { + return redisTool.getUseStringAntiCachePassThrough( + redisKey, + getter, + JSON::toJSONString, + jsonString -> JSON.parseObject(jsonString, NoteType.class), + 30, + TimeUnit.MINUTES + ); + } + + public String getUserAllNoteTypeRedisKey(int userId) { + return RedisKey.join(RedisKey.NoteType.USER_ALL, String.valueOf(userId)); + } + + public String getNoteTypeRedisKeyById(int noteTypeId) { + return RedisKey.join(RedisKey.NoteType.ID, String.valueOf(noteTypeId)); + } + + /** + * 获取笔记类型(使用redis做缓存,有防缓存击穿) + */ + public NoteType getByIdWithRedis(int noteTypeId) { + return getWithRedis(getNoteTypeRedisKeyById(noteTypeId), () -> getById(noteTypeId)); + } + + public void deleteByIdInRedis(int noteTypeId) { + stringRedisTemplate.delete(getNoteTypeRedisKeyById(noteTypeId)); + } + + /** + * 新增笔记类型 + */ + public void addNoteType(User user, NoteTypeAddForm noteTypeAddForm) { + NoteType noteType = BeanUtil.copyProperties(noteTypeAddForm, NoteType.class); + noteType.setUserId(user.getId()); + boolean success = this.save(noteType); + if (!success) { + throw new NoteTypeServiceException(); + } + + deleteUserAllNoteTypeInRedis(user.getId()); + } + + /** + * 删除笔记类型 + */ + public void deleteNoteTypeByIds(User user, List noteTypeIds) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(NoteType::getId, noteTypeIds); + queryWrapper.eq(NoteType::getUserId, user.getId()); + List noteTypeList = this.list(queryWrapper); + + noteTypeList.forEach(noteType -> { + LambdaQueryWrapper noteQueryWrapper = new LambdaQueryWrapper<>(); + noteQueryWrapper.eq(Note::getTypeId, noteType.getId()); + long count = noteService.count(noteQueryWrapper); + if (count > 0) { + throw new NoteTypeServiceException(String.format("请先删除或转移<%s>类型下的笔记", noteType.getName())); + } + }); + this.removeBatchByIds(noteTypeList); + + // 清除 redis 中的缓存 + noteTypeList.forEach(noteType -> deleteByIdInRedis(noteType.getId())); + deleteUserAllNoteTypeInRedis(user.getId()); + } + + /** + * 更新笔记类型 + */ + public void updateNoteType(User user, NoteTypeUpdateForm noteTypeUpdateForm) { + NoteType noteType = BeanUtil.copyProperties(noteTypeUpdateForm, NoteType.class); + noteType.setUserId(user.getId()); + boolean updateSuccess = this.updateById(noteType); + if (!updateSuccess) { + throw new NoteTypeServiceException("更新失败"); + } + + // 清除 redis 中的缓存 + deleteByIdInRedis(noteType.getId()); + deleteUserAllNoteTypeInRedis(user.getId()); + } + + /** + * 获取用户所有笔记类型 + */ + public List getUserAllNoteType(int userId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(NoteType::getUserId, userId); + List listInDatabase = this.list(queryWrapper); + + if (listInDatabase.isEmpty()) { + NoteType defaultNoteType = createDefaultAndSetToNoteOfNoneType(userId); + return Collections.singletonList(defaultNoteType); + } + + return listInDatabase; + } + + public NoteType createDefaultAndSetToNoteOfNoneType(int userId) { + NoteType defaultNoteType = new NoteType(); + defaultNoteType.setUserId(userId); + defaultNoteType.setName("默认"); + this.save(defaultNoteType); + + LambdaQueryWrapper queryNoteWrapper = new LambdaQueryWrapper<>(); + queryNoteWrapper.eq(Note::getUserId, userId); + queryNoteWrapper.isNull(Note::getTypeId); + List notesOfNoneType = noteService.list(queryNoteWrapper); + notesOfNoneType.forEach(note -> note.setTypeId(defaultNoteType.getId())); + noteService.updateBatchByIdWithRedisAndEs(notesOfNoneType); + return defaultNoteType; + } + + /** + * 获取用户所有笔记类型(使用redis做缓存,有防缓存击穿) + */ + public List getUserAllNoteTypeWithRedis(int userId) { + return redisTool.getUseStringAntiCachePassThrough( + getUserAllNoteTypeRedisKey(userId), + () -> getUserAllNoteType(userId), + JSON::toJSONString, + jsonString -> JSON.parseArray(jsonString, NoteType.class), + 30, + TimeUnit.MINUTES + ); + } + + /** + * 删除 redis 中用户所有笔记类型 + */ + public void deleteUserAllNoteTypeInRedis(int userId) { + stringRedisTemplate.delete(getUserAllNoteTypeRedisKey(userId)); + } +} diff --git a/src/main/java/com/xclhove/xnote/service/ShareNoteRecordService.java b/src/main/java/com/xclhove/xnote/service/ShareNoteRecordService.java new file mode 100644 index 0000000..aa29dc2 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/service/ShareNoteRecordService.java @@ -0,0 +1,143 @@ +package com.xclhove.xnote.service; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.lang.UUID; +import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xclhove.xnote.constant.RedisKey; +import com.xclhove.xnote.exception.ShareNoteRecordServiceException; +import com.xclhove.xnote.mapper.ShareNoteRecordMapper; +import com.xclhove.xnote.pojo.form.shareNoteRecord.ShareNoteRecordCreateForm; +import com.xclhove.xnote.pojo.form.shareNoteRecord.ShareNoteRecordUpdateForm; +import com.xclhove.xnote.pojo.table.ShareNoteRecord; +import com.xclhove.xnote.pojo.table.User; +import com.xclhove.xnote.pojo.vo.ShareNoteRecordVO; +import com.xclhove.xnote.tool.RedisTool; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * @author xclhove + */ +@Service +@Slf4j +@RequiredArgsConstructor +public class ShareNoteRecordService extends ServiceImpl { + private final StringRedisTemplate stringRedisTemplate; + private final ShareNoteRecordMapper shareNoteRecordMapper; + private final RedisTool redisTool; + + public ShareNoteRecord getWithRedis(String redisKey, RedisTool.Getter getter) { + return redisTool.getUseStringAntiCachePassThrough( + redisKey, + getter, + JSON::toJSONString, + jsonString -> JSON.parseObject(jsonString, ShareNoteRecord.class), + 30, + TimeUnit.MINUTES + ); + } + + public String generateCode() { + return UUID.randomUUID().toString(true); + } + + public String getRedisKeyByCode(String code) { + return RedisKey.join(RedisKey.ShareNote.CODE, code); + } + + public String getRedisKeyById(Integer id) { + return RedisKey.join(RedisKey.ShareNote.ID, String.valueOf(id)); + } + + public String share(User noteOwner, ShareNoteRecordCreateForm shareNoteRecordCreateForm) { + String code = generateCode(); + + ShareNoteRecord shareNoteRecord = BeanUtil.copyProperties(shareNoteRecordCreateForm, ShareNoteRecord.class); + shareNoteRecord.setCode(code); + shareNoteRecord.setUserId(noteOwner.getId()); + boolean saveSuccess = this.save(shareNoteRecord); + if (!saveSuccess) { + throw new ShareNoteRecordServiceException(); + } + return code; + } + + public ShareNoteRecord getByCode(String code) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(ShareNoteRecord::getCode, code); + return this.getOne(queryWrapper); + } + + public ShareNoteRecord getByCodeWithRedis(String code) { + return getWithRedis(getRedisKeyByCode(code), () -> getByCode(code)); + } + + public void deleteByCodeInRedis(String code) { + String redisKey = getRedisKeyByCode(code); + stringRedisTemplate.delete(redisKey); + } + + public void deleteShareNotes(User user, List shareNoteIds) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(ShareNoteRecord::getUserId, user.getId()); + queryWrapper.in(ShareNoteRecord::getId, shareNoteIds); + List deleteRecords = this.list(queryWrapper); + boolean removeSuccess = this.removeBatchByIds(deleteRecords); + if (!removeSuccess) { + throw new ShareNoteRecordServiceException(); + } + + Set redisKeys = new HashSet<>(); + deleteRecords.forEach(shareNoteRecord -> { + redisKeys.add(getRedisKeyByCode(shareNoteRecord.getCode())); + redisKeys.add(getRedisKeyById(shareNoteRecord.getId())); + }); + stringRedisTemplate.delete(redisKeys); + } + + public Page getShareNoteList(User user, Page page) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + Page pageResult = shareNoteRecordMapper.pageUserShareNote(user.getId(), page, queryWrapper); + return pageResult; + } + + public void deleteById(ShareNoteRecord shareNoteRecord) { + this.removeById(shareNoteRecord.getId()); + deleteByCodeInRedis(shareNoteRecord.getCode()); + } + + public ShareNoteRecord getByIdWithRedis(Integer id) { + return getWithRedis(getRedisKeyById(id), () -> this.getById(id)); + } + + public void deleteByIdInRedis(Integer id) { + String redisKey = getRedisKeyById(id); + stringRedisTemplate.delete(redisKey); + } + + public void update(User user, ShareNoteRecordUpdateForm shareNoteRecordUpdateForm) { + ShareNoteRecord shareNoteRecord = this.getById(shareNoteRecordUpdateForm.getId()); + if (shareNoteRecord == null || !shareNoteRecord.getUserId().equals(user.getId())) { + throw new ShareNoteRecordServiceException("分享记录不存在"); + } + BeanUtil.copyProperties(shareNoteRecordUpdateForm, shareNoteRecord); + boolean updateSuccess = this.updateById(shareNoteRecord); + if (!updateSuccess) { + throw new ShareNoteRecordServiceException("系统异常,更新失败"); + } + + // 删除 redis 中的缓存 + deleteByCodeInRedis(shareNoteRecord.getCode()); + deleteByIdInRedis(shareNoteRecord.getId()); + } +} diff --git a/src/main/java/com/xclhove/xnote/service/UserImageService.java b/src/main/java/com/xclhove/xnote/service/UserImageService.java new file mode 100644 index 0000000..8c0b097 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/service/UserImageService.java @@ -0,0 +1,139 @@ +package com.xclhove.xnote.service; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xclhove.xnote.constant.RedisKey; +import com.xclhove.xnote.exception.UserImageServiceException; +import com.xclhove.xnote.mapper.ImageMapper; +import com.xclhove.xnote.mapper.UserImageMapper; +import com.xclhove.xnote.pojo.table.Image; +import com.xclhove.xnote.pojo.table.User; +import com.xclhove.xnote.pojo.table.UserImage; +import com.xclhove.xnote.pojo.vo.SearchUserImageVO; +import com.xclhove.xnote.tool.RedisTool; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; + +import java.sql.Timestamp; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @author xclhove + */ +@Service +@Slf4j +@RequiredArgsConstructor +public class UserImageService extends ServiceImpl { + private final RedisTool redisTool; + private final StringRedisTemplate stringRedisTemplate; + private final ImageService imageService; + private final ImageMapper imageMapper; + + public UserImage getWithRedis(String redisKey, RedisTool.Getter getter) { + return redisTool.getUseStringAntiCachePassThrough( + redisKey, + getter, + JSON::toJSONString, + json -> JSON.parseObject(json, UserImage.class), + 30, + TimeUnit.MINUTES + ); + } + + public String getRedisKeyByUserAndImageId(Integer userId, Integer imageId) { + return RedisKey.join(RedisKey.UserImage.USER_AND_IMAGE_ID, userId.toString(), imageId.toString()); + } + + @Nullable + public UserImage getByUserAndImageId(Integer userId,Integer imageId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(UserImage::getUserId, userId); + queryWrapper.eq(UserImage::getImageId, imageId); + return this.getOne(queryWrapper); + } + + @Nullable + public UserImage getByUserAndImageIdWithRedis(Integer userId, Integer imageId) { + return getWithRedis( + getRedisKeyByUserAndImageId(userId, imageId), + () -> getByUserAndImageId(userId, imageId) + ); + } + + public List listByUserId(Integer userId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(UserImage::getUserId, userId); + return this.list(queryWrapper); + } + + public boolean saveWithRedis(UserImage userImage) { + boolean saveSuccess = this.save(userImage); + stringRedisTemplate.delete(getRedisKeyByUserAndImageId(userImage.getUserId(), userImage.getImageId())); + return saveSuccess; + } + + public void removeUserImageByIds(@NonNull User user, @NonNull List userImageIds) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(UserImage::getUserId, user.getId()); + queryWrapper.in(UserImage::getId, userImageIds); + List removeUserImages = list(queryWrapper); + if (removeUserImages.isEmpty()) { + return; + } + boolean removeSuccess = removeBatchByIds(removeUserImages); + if (!removeSuccess) { + throw new UserImageServiceException("系统异常,删除失败"); + } + + List removeImageIds = removeUserImages.stream().map(UserImage::getImageId).collect(Collectors.toList()); + imageMapper.incrementImageOwnerCount(removeImageIds, -1); + + Set redisKeys = new HashSet<>(); + removeUserImages.forEach(userImage -> redisKeys.add(getRedisKeyByUserAndImageId( + userImage.getUserId(), + userImage.getImageId() + ))); + redisKeys.add(imageService.getUserImageTotalSizeRedisKey(user.getId())); + stringRedisTemplate.delete(redisKeys); + } + + public Page searchUserImage(@NonNull Page page, @NonNull User user, @Nullable String search) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(UserImage::getUserId, user.getId()); + queryWrapper.like(StrUtil.isNotBlank(search), UserImage::getAlias, search); + Page pageResult = this.page(page, queryWrapper); + List searchUserImageVOList = BeanUtil.copyToList( + pageResult.getRecords(), + SearchUserImageVO.class + ); + + Set imageIds = new HashSet<>(); + searchUserImageVOList.forEach(searchUserImageVO -> imageIds.add(searchUserImageVO.getImageId())); + + if (!imageIds.isEmpty()) { + LambdaQueryWrapper imageLambdaQueryWrapper = new LambdaQueryWrapper<>(); + imageLambdaQueryWrapper.in(Image::getId, imageIds); + List images = imageService.list(imageLambdaQueryWrapper); + Map imageLastDownloadTimeMap = new HashMap<>(images.size()); + images.forEach(image -> imageLastDownloadTimeMap.put(image.getId(), image.getLastDownloadTime())); + searchUserImageVOList.forEach(searchUserImageVO -> { + searchUserImageVO.setLastDownloadTime(imageLastDownloadTimeMap.get(searchUserImageVO.getImageId())); + }); + } + + Page pageVO = new Page<>(); + BeanUtil.copyProperties(pageResult, pageVO); + pageVO.setRecords(searchUserImageVOList); + return pageVO; + } +} diff --git a/src/main/java/com/xclhove/xnote/service/UserService.java b/src/main/java/com/xclhove/xnote/service/UserService.java index 4926cb7..cafae1d 100644 --- a/src/main/java/com/xclhove/xnote/service/UserService.java +++ b/src/main/java/com/xclhove/xnote/service/UserService.java @@ -1,73 +1,264 @@ package com.xclhove.xnote.service; -import com.baomidou.mybatisplus.extension.service.IService; -import com.xclhove.xnote.entity.table.User; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xclhove.xnote.constant.RedisKey; +import com.xclhove.xnote.exception.UserServiceException; +import com.xclhove.xnote.mapper.UserMapper; +import com.xclhove.xnote.pojo.dto.UserDTO; +import com.xclhove.xnote.pojo.enums.UserStatus; +import com.xclhove.xnote.pojo.form.user.UserUpdateEmailForm; +import com.xclhove.xnote.pojo.form.user.UserUpdateForm; +import com.xclhove.xnote.pojo.form.user.UserUpdatePasswordForm; +import com.xclhove.xnote.pojo.table.User; +import com.xclhove.xnote.tool.RedisTool; +import com.xclhove.xnote.tool.UserTokenTool; +import com.xclhove.xnote.util.EncryptUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Set; +import java.util.concurrent.TimeUnit; /** * @author xclhove */ -public interface UserService extends IService { +@Service +@RequiredArgsConstructor +public class UserService extends ServiceImpl { + private final StringRedisTemplate stringRedisTemplate; + private final RedisTool redisTool; + + private User getWithRedis(String redisKey, RedisTool.Getter getter) { + return redisTool.getUseStringAntiCachePassThrough( + redisKey, + getter, + user -> { + UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); + return JSON.toJSONString(userDTO); + }, + jsonString -> { + UserDTO userDTO = JSON.parseObject(jsonString, UserDTO.class); + return BeanUtil.copyProperties(userDTO, User.class); + }, + 30, + TimeUnit.MINUTES + ); + } + + public String getTokenRedisKey(int userId, String deviceId) { + return RedisKey.join(RedisKey.User.TOKEN, String.valueOf(userId), deviceId); + } + + private String getRedisKeyById(int userId) { + return RedisKey.join(RedisKey.User.ID, String.valueOf(userId)); + } + + private String getRedisKeyByAccount(String account) { + return RedisKey.join(RedisKey.User.ACCOUNT, account); + } + + private String getRedisKeyByEmail(String email) { + return RedisKey.join(RedisKey.User.EMAIL, email); + } + + /** + * 通过账号获取用户信息 + */ + public User getByAccount(String account) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(User::getAccount, account); + return this.getOne(queryWrapper); + } + + /** + * 通过邮箱获取用户信息 + */ + public User getByEmail(String email) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(User::getEmail, email); + return this.getOne(queryWrapper); + } + + /** + * 通过ID获取用户信息(使用redis做缓存,有防缓存穿透) + */ + public User getByIdWithRedis(int userId) { + return getWithRedis(getRedisKeyById(userId), () -> getById(userId)); + } + + /** + * 通过账号获取用户信息(使用redis做缓存,有防缓存穿透) + */ + public User getByAccountWithRedis(String account) { + return getWithRedis(getRedisKeyByAccount(account), () -> getByAccount(account)); + } + + /** + * 通过邮箱获取用户信息(使用redis做缓存,有防缓存穿透) + */ + public User getByEmailWithRedis(String email) { + return getWithRedis(getRedisKeyByEmail(email), () -> getByEmail(email)); + } + /** - * 用户注册 - * - * @param user User对象 - * @return 是否注册成功 + * 密码加密 + * @param account 账号 + * @param password 未加密的密码 + * @return 加密后的密码 */ - boolean register(User user); + private String encryptPassword(String account, String password) { + return EncryptUtil.encrypt(password, account, EncryptUtil.EncryptionAlgorithm.SHA256); + } /** - * 用户注册 - * - * @param account 账号 - * @param password 密码 - * @return token字符串 + * 注册 */ - String login(String account, String password); + public void register(User user) { + //判断账号是否存在 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(User::getAccount, user.getAccount()); + if (this.count(queryWrapper) > 0) { + throw new UserServiceException("账号已存在"); + } + + // 判断邮箱是否已注册 + queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(User::getEmail, user.getEmail()); + if (this.count(queryWrapper) > 0) { + throw new UserServiceException("邮箱已被注册"); + } + + // 密码加密 + String encryptPassword = encryptPassword(user.getAccount(), user.getPassword()); + user.setPassword(encryptPassword); + + user.setId(null); + user.setStatus(UserStatus.ENABLE); + user.setHomePageNoteId(null); + boolean success = save(user); + if (!success) { + throw new UserServiceException("系统异常,注册失败"); + } + + stringRedisTemplate.delete(getRedisKeyByAccount(user.getAccount())); + stringRedisTemplate.delete(getRedisKeyByEmail(user.getEmail())); + } /** - * 用户注销 - * - * @param userId 用户id + * 登录 */ - void logout(Integer userId); + public String login(User user, String deviceId) { + User userInDatabase = this.getByAccountWithRedis(user.getAccount()); + if (userInDatabase == null) { + throw new UserServiceException("账号不存在"); + } + + String encryptPassword = encryptPassword(user.getAccount(), user.getPassword()); + if (!userInDatabase.getPassword().equals(encryptPassword)) { + throw new UserServiceException("密码错误"); + } + + String redisKey = getTokenRedisKey(userInDatabase.getId(), deviceId); + String tokenInRedis = stringRedisTemplate.opsForValue().get(redisKey); + if (StrUtil.isNotBlank(tokenInRedis)) { + return tokenInRedis; + } + + String newToken = UserTokenTool.generate(encryptPassword, userInDatabase.getId()); + stringRedisTemplate.opsForValue().set(redisKey, newToken, 2, TimeUnit.HOURS); + return newToken; + } /** - * 用户修改信息 - * - * @param user User对象 - * @return 是否更新成功 + * 注销 */ - boolean updateInfo(User user); + public void logout(User user, String deviceId) { + String redisKey = getTokenRedisKey(user.getId(), deviceId); + Boolean success = stringRedisTemplate.delete(redisKey); + + if (success == null || !success) { + throw new UserServiceException("登出失败"); + } + } /** - * 通过用户id查询用户信息 - * - * @param userId 用户id - * @return User对象 + * 更新用户信息(使用redis) */ - User queryById(Integer userId); + private boolean updateByIdWithRedis(User user) { + boolean success = updateById(user); + if (!success) { + return false; + } + + // redis + User userInRedis = getByIdWithRedis(user.getId()); + stringRedisTemplate.delete(getRedisKeyById(userInRedis.getId())); + stringRedisTemplate.delete(getRedisKeyByAccount(userInRedis.getAccount())); + + return true; + } /** - * 通过用户id禁封用户 - * - * @param userId 用户id - * @return 是否禁封成功 + * 更新用户信息,不包含密码和邮箱 */ - boolean banById(Integer userId); + public void updateCommonInfo(User user, UserUpdateForm userUpdateForm) { + User newUser = BeanUtil.copyProperties(userUpdateForm, User.class); + newUser.setId(user.getId()); + + boolean success = updateByIdWithRedis(newUser); + if (!success) { + throw new UserServiceException(); + } + } /** - * 发送验证码 - * - * @param email 邮箱 + * 更新密码 */ - void sendVerificationCode(String email); + public void updatePassword(User user, UserUpdatePasswordForm userUpdatePasswordForm) { + User newUser = new User(); + newUser.setId(user.getId()); + String encryptPassword = encryptPassword(user.getAccount(), userUpdatePasswordForm.getPassword()); + newUser.setPassword(encryptPassword); + boolean success = updateByIdWithRedis(user); + if (!success) { + throw new UserServiceException(); + } + + String redisKey = getTokenRedisKey(user.getId(), "*"); + Set keys = stringRedisTemplate.keys(redisKey); + if (keys != null && !keys.isEmpty()) { + stringRedisTemplate.delete(keys); + } + } /** - * 校验验证码 - * - * @param email 邮箱 - * @param verificationCode 验证码 - * @return 是否验证成功 + * 更新邮箱 */ - boolean verifyVerificationCode(String email, String verificationCode); + public void updateEmail(User user, UserUpdateEmailForm userUpdateEmailForm) { + String newEmail = userUpdateEmailForm.getEmail(); + if(user.getEmail().equals(newEmail)) { + return; + } + + User newEmailOwner = getByEmailWithRedis(newEmail); + if (newEmailOwner != null) { + throw new UserServiceException("邮箱已绑定,请先换绑"); + } + + User newUser = new User(); + newUser.setId(user.getId()); + newUser.setEmail(newEmail); + + boolean success = updateByIdWithRedis(newUser); + if (!success) { + throw new UserServiceException(); + } + + stringRedisTemplate.delete(getRedisKeyByEmail(newEmail)); + } } diff --git a/src/main/java/com/xclhove/xnote/service/VerificationCodeService.java b/src/main/java/com/xclhove/xnote/service/VerificationCodeService.java new file mode 100644 index 0000000..403845f --- /dev/null +++ b/src/main/java/com/xclhove/xnote/service/VerificationCodeService.java @@ -0,0 +1,108 @@ +package com.xclhove.xnote.service; + +import cn.hutool.core.util.StrUtil; +import com.xclhove.xnote.config.XnoteConfig; +import com.xclhove.xnote.constant.RedisKey; +import com.xclhove.xnote.exception.VerificationCodeException; +import com.xclhove.xnote.tool.EmailTool; +import com.xclhove.xnote.util.VerificationCodeUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import javax.mail.MessagingException; +import java.util.concurrent.TimeUnit; + +/** + * @author xclhove + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class VerificationCodeService { + private static final int VERIFICATION_CODE_LENGTH = 6; + private static final int VERIFICATION_CODE_EXPIRE_SECONDS = 60; + + private final EmailTool emailTool; + private final StringRedisTemplate stringRedisTemplate; + private final XnoteConfig xnoteConfig; + + private String getRedisKey(String email) { + return RedisKey.join(RedisKey.VerificationCode.EMAIL, email); + } + + private String generate() { + return VerificationCodeUtil.generate(VERIFICATION_CODE_LENGTH, true); + } + + private void saveToRedis(String code, String email) { + String redisKey = getRedisKey(email); + stringRedisTemplate.opsForValue().set(redisKey, code, VERIFICATION_CODE_EXPIRE_SECONDS, TimeUnit.SECONDS); + } + + private void deleteInRedis(String email) { + stringRedisTemplate.delete(getRedisKey(email)); + } + + private String getFromRedis(String email) { + String redisKey = getRedisKey(email); + return stringRedisTemplate.opsForValue().get(redisKey); + } + + private int getExpireSecond(String email) { + String redisKey = getRedisKey(email); + return Math.toIntExact(stringRedisTemplate.getExpire(redisKey, TimeUnit.SECONDS)); + } + + public boolean verify(String code, String email) { + if (!xnoteConfig.debug.getEnableVerifyVerificationCode()) { + return true; + } + + String codeInRedis = getFromRedis(email); + boolean valid = code.equalsIgnoreCase(codeInRedis); + if (valid) { + deleteInRedis(email); + } + return valid; + } + + public int sendToEmail(String email) { + String codeInRedis = getFromRedis(email); + if (StrUtil.isNotBlank(codeInRedis)) { + int expireSecond = getExpireSecond(email); + + StringBuilder content = new StringBuilder(); + content.append("您的验证码是:") + .append(codeInRedis) + .append(",请在") + .append(expireSecond) + .append("秒内使用。"); + if (xnoteConfig.debug.getEnableMockSendVerificationCode()) { + log.info(content.toString()); + } + return expireSecond; + } + + String newCode = generate(); + StringBuilder content = new StringBuilder(); + content.append("您的验证码是:") + .append(newCode) + .append(",请在") + .append(VERIFICATION_CODE_EXPIRE_SECONDS) + .append("秒内使用。"); + try { + if (xnoteConfig.debug.getEnableMockSendVerificationCode()) { + log.info(content.toString()); + } else { + emailTool.sendMail(email, "XNote验证码", content.toString()); + } + saveToRedis(newCode, email); + return VERIFICATION_CODE_EXPIRE_SECONDS; + } catch (MessagingException e) { + log.error("发送验证码失败", e); + throw new VerificationCodeException(); + } + } +} diff --git a/src/main/java/com/xclhove/xnote/service/impl/AdminServiceImpl.java b/src/main/java/com/xclhove/xnote/service/impl/AdminServiceImpl.java deleted file mode 100644 index c82a018..0000000 --- a/src/main/java/com/xclhove/xnote/service/impl/AdminServiceImpl.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.xclhove.xnote.service.impl; - -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.xclhove.xnote.entity.table.Admin; -import com.xclhove.xnote.exception.AdminServiceException; -import com.xclhove.xnote.mapper.AdminMapper; -import com.xclhove.xnote.service.AdminService; -import com.xclhove.xnote.util.EncryptUtil; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.List; - -/** - * @author xclhove - */ -@Service -@Slf4j -public class AdminServiceImpl extends ServiceImpl implements AdminService { - @Override - public boolean addAdmin(Admin admin) { - String password = admin.getPassword(); - password = EncryptUtil.encrypt(password, admin.getAccount(), EncryptUtil.EncryptionAlgorithm.SHA256); - admin.setPassword(password); - boolean addSuccess = false; - try { - addSuccess = this.save(admin); - } catch (Exception e) { - log.error(e.toString()); - throw new AdminServiceException("出现异常,添加管理员失败!"); - } - if (!addSuccess) throw new AdminServiceException("添加管理员失败!"); - return true; - } - - @Override - public boolean deleteAdmin(Integer id) { - return true; - } - - @Override - public boolean changeAdmin(Admin admin) { - return true; - } - - @Override - public Admin getAdmin(Integer id) { - return null; - } - - @Override - public List getAdmins(Integer pageNumber, Integer pageSize) { - return null; - } -} diff --git a/src/main/java/com/xclhove/xnote/service/impl/ImageServiceImpl.java b/src/main/java/com/xclhove/xnote/service/impl/ImageServiceImpl.java deleted file mode 100644 index 36b0152..0000000 --- a/src/main/java/com/xclhove/xnote/service/impl/ImageServiceImpl.java +++ /dev/null @@ -1,250 +0,0 @@ -package com.xclhove.xnote.service.impl; - -import cn.hutool.core.util.StrUtil; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.xclhove.xnote.entity.dto.ImagePageDTO; -import com.xclhove.xnote.entity.table.Image; -import com.xclhove.xnote.exception.ImageServiceException; -import com.xclhove.xnote.exception.NoteServiceException; -import com.xclhove.xnote.mapper.ImageMapper; -import com.xclhove.xnote.service.ImageService; -import com.xclhove.xnote.tool.MinioTool; -import com.xclhove.xnote.util.ExceptionUtil; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletResponse; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/** - *

- * 图片 服务实现类 - *

- * - * @author xclhove - * @since 2023-12-09 - */ -@Service -@RequiredArgsConstructor -public class ImageServiceImpl extends ServiceImpl implements ImageService { - /** 允许上传的文件格式 */ - @Value("${xnote.image.allow-type}") - private String allowType; - - /** 允许上传的文件大小,默认10MB */ - @Value("${xnote.image.allow-size:10485670}") - private Long allowSize; - - private final MinioTool minio; - - @Override - public Image upload(Integer userId, MultipartFile imageFile) { - if (imageFile.isEmpty() || imageFile.getSize() == 0) { - throw new NoteServiceException("不允许上传空文件"); - } - String contentType = imageFile.getContentType(); - if (!contentType.matches(allowType)) { - throw new NoteServiceException("不允许上传该格式的图片!"); - } - if (imageFile.getSize() > allowSize) { - throw new NoteServiceException("上传的文件大小不能超过" + allowSize + "字节!"); - } - String fileName = null; - try { - fileName = minio.upload(imageFile); - } catch (Exception e) { - log.error(ExceptionUtil.getMessage(e)); - throw new ImageServiceException("图片文件上传失败!"); - } - Image image = new Image(); - image.setUserId(userId); - image.setAlias(imageFile.getOriginalFilename()); - image.setName(fileName); - boolean save = false; - try { - save = this.save(image); - } catch (Exception e) { - log.error(ExceptionUtil.getMessage(e)); - throw new ImageServiceException(" 出现异常,图片文件上传成功,数据库保存失败!"); - } - if (!save) { - throw new ImageServiceException("图片文件上传成功,数据库保存失败!"); - } - return image; - } - - @Override - public boolean deleteByIds(Integer userId, List imageIds) { - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(userId != null, Image::getUserId, userId) - .and(qw -> qw.in(Image::getId, imageIds)); - List images = this.list(queryWrapper); - List hasDeleteImageIds = new ArrayList<>(); - try { - for (Image image : images) { - deleteImageFile(image.getName()); - hasDeleteImageIds.add(image.getId()); - } - } catch (ImageServiceException e) { - throw e; - } catch (Exception e) { - log.error(ExceptionUtil.getMessage(e)); - throw new ImageServiceException("出现异常,删除图片失败!"); - } finally { - if (hasDeleteImageIds.isEmpty()) return false; - - LambdaQueryWrapper deleteWrapper = new LambdaQueryWrapper<>(); - deleteWrapper.eq(userId != null, Image::getUserId, userId) - .and(qw -> qw.in(Image::getId, hasDeleteImageIds)); - this.remove(deleteWrapper); - } - return true; - } - - private void deleteImageFile(String imageName) { - boolean deleteSuccess = false; - if (StrUtil.isBlank(imageName)) throw new ImageServiceException("图片名不能为空!"); - try { - deleteSuccess = minio.deleteFileByFileName(imageName); - } catch (Exception e) { - log.error(ExceptionUtil.getMessage(e)); - throw new ImageServiceException("出现异常,删除图片文件失败!"); - } - if (!deleteSuccess) throw new ImageServiceException("图片删除失败!"); - } - - @Override - public Image get(Integer userId, Integer imageId) { - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(userId != null, Image::getUserId, userId) - .and(qw -> qw.eq(Image::getId, imageId)); - try { - return this.getOne(queryWrapper); - } catch (Exception e) { - log.error(e.getMessage()); - throw new ImageServiceException("出现异常,获取图片信息失败!"); - } - } - - @Override - public ImagePageDTO page(ImagePageDTO pageDTO) { - Page page = new Page<>(pageDTO.getCurrent(), pageDTO.getSize()); - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(pageDTO.getUserId() != null, Image::getUserId, pageDTO.getUserId()) - .like(pageDTO.getSearchAlias() != null, Image::getAlias, pageDTO.getSearchAlias()) - .orderByDesc(Image::getLastDownloadTime); - try { - Page pageImage = this.page(page, queryWrapper); - List images = pageImage.getRecords(); - pageDTO.setList(images); - pageDTO.setTotal((int) pageImage.getTotal()); - return pageDTO; - } catch (Exception e) { - log.error(e.getMessage()); - throw new ImageServiceException("出现异常,分页查询失败!"); - } - } - - @Override - public boolean change(Image image) { - LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); - updateWrapper.eq(Image::getId, image.getId()) - .eq(image.getUserId() != null, Image::getUserId, image.getUserId()) - .set(image.getAlias() != null, Image::getAlias, image.getAlias()); - boolean updateSuccess = false; - try { - updateSuccess = this.updateById(image); - } catch (Exception e) { - log.error(e.getMessage()); - throw new ImageServiceException("出现异常,修改图片信息失败!"); - } - if (!updateSuccess) throw new ImageServiceException("修改图片信息失败!"); - return true; - } - - @Override - public void downloadById(HttpServletResponse response, Integer userId, Integer imageId) { - Image image = null; - try { - image = this.getById(imageId); - } catch (Exception e) { - log.error(ExceptionUtil.getMessage(e)); - throw new ImageServiceException("出现异常,获取图片信息失败!"); - } - if (!Objects.equals(userId, image.getUserId())) { - throw new ImageServiceException("您无权下载该图片!"); - } - download(response, image); - } - - @Override - public void downloadByName(HttpServletResponse response, String imageName) { - Image image = null; - try { - // 获取图片信息 - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(Image::getName, imageName); - image = this.getOne(queryWrapper); - - // 更新图片下载时间 - image.setLastDownloadTime(Timestamp.valueOf(LocalDateTime.now())); - this.updateById(image); - } catch (Exception e) { - log.error(ExceptionUtil.getMessage(e)); - throw new ImageServiceException("出现异常,获取图片信息或更新图片下载时间失败!"); - } - - download(response, image); - } - - @Override - public void download(HttpServletResponse response, Image image) { - if (image == null) { - throw new ImageServiceException("图片不存在或已被删除!"); - } - - byte[] bytes = new byte[0]; - try { - bytes = minio.downloadFile(image.getName()); - } catch (Exception e) { - log.error(ExceptionUtil.getMessage(e)); - throw new ImageServiceException("出现异常,获取图片文件失败!"); - } - - // 设置文件名称 - String imageFileName = image.getAlias(); - imageFileName = imageFileName.replaceAll("\\s+", "_"); - // 进行URL编码,防止乱码 - try { - imageFileName = URLEncoder.encode(imageFileName, "UTF-8"); - } catch (UnsupportedEncodingException e) { - log.error(ExceptionUtil.getMessage(e)); - throw new ImageServiceException("出现异常,编码图片文件名失败!"); - } - response.addHeader("Content-Disposition", "attachment;fileName=" + imageFileName); - - String contentType = "image/" + image.getName().substring(image.getName().lastIndexOf(".") + 1); - response.setContentType(contentType); - // 传输文件 - try { - ServletOutputStream stream = response.getOutputStream(); - stream.write(bytes); - stream.flush(); - } catch (Exception e) { - log.error(ExceptionUtil.getMessage(e)); - throw new ImageServiceException("出现异常,图片传输失败!"); - } - } -} diff --git a/src/main/java/com/xclhove/xnote/service/impl/NoteServiceImpl.java b/src/main/java/com/xclhove/xnote/service/impl/NoteServiceImpl.java deleted file mode 100644 index cad03c4..0000000 --- a/src/main/java/com/xclhove/xnote/service/impl/NoteServiceImpl.java +++ /dev/null @@ -1,164 +0,0 @@ -package com.xclhove.xnote.service.impl; - -import cn.hutool.core.util.StrUtil; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.xclhove.xnote.entity.dto.NotePageDTO; -import com.xclhove.xnote.entity.table.Note; -import com.xclhove.xnote.enums.entityattribute.NoteIsPublic; -import com.xclhove.xnote.enums.result.ResultType; -import com.xclhove.xnote.exception.NoteAccessCodeException; -import com.xclhove.xnote.exception.NoteServiceException; -import com.xclhove.xnote.mapper.NoteMapper; -import com.xclhove.xnote.service.NoteService; -import com.xclhove.xnote.util.ExceptionUtil; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.List; - -/** - * @author xclhove - */ -@Service -@Slf4j -public class NoteServiceImpl extends ServiceImpl implements NoteService { - @Value("${xnote.page-request.max-page-size: 200}") - private Integer maxPageSize; - - @Override - public Note getNoteById(Integer userId, Integer noteId, String accessCode) { - Note note = this.getById(noteId); - if (note == null) { - throw new NoteServiceException("没有该笔记!"); - } - - // 用户自己的笔记无需访问码 - if (note.getUserId().equals(userId)) { - return note; - } - - // 笔记是公开的可以访问 - if (note.getIsPublic() == NoteIsPublic.YES) { - return note; - } - - // 笔记是非公开的且无访问码,说明该笔记是不让其他人访问 - if (StrUtil.isBlank(note.getAccessCode())) { - throw new NoteServiceException("无权访问该笔记!"); - } - - // 笔记是非公开的但有访问码,检验访问码是否为空 - if (StrUtil.isBlank(accessCode)) { - note.setContent(null); - note.setAccessCode(null); - throw new NoteAccessCodeException("需要访问码!", note); - } - - // 笔记是非公开的但有访问码,检验访问码是否正确 - if (!note.getAccessCode().equals(accessCode)) { - note.setContent(null); - note.setAccessCode(null); - throw new NoteAccessCodeException(ResultType.NOTE_ACCESS_CODE_EXCEPTION.getMessage(), note); - } - - return note; - } - - @Override - public Note addNote(Note note) { - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(Note::getUserId, note.getUserId()) - .eq(Note::getTitle, note.getTitle()); - Note existNote = this.getOne(queryWrapper); - if (existNote != null) throw new NoteServiceException("该标题的笔记已存在!"); - - String title = note.getTitle(); - if (StrUtil.isBlank(title)) title = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); - note.setTitle(title); - boolean saveSuccess = false; - try { - saveSuccess = this.save(note); - } catch (Exception e) { - log.error(e.getMessage()); - throw new NoteServiceException("出现异常,保存失败!"); - } - if (!saveSuccess) throw new NoteServiceException("保存失败!"); - note.setKeywords(null); - note.setContent(null); - return note; - } - - @Override - public Note updateNote(Note note) { - boolean updateSuccess = false; - try { - LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); - updateWrapper.eq(Note::getId, note.getId()); - updateWrapper.eq(Note::getUserId, note.getUserId()); - updateSuccess = this.update(note, updateWrapper); - } catch (Exception e) { - log.error(ExceptionUtil.getMessage(e)); - throw new NoteServiceException("出现异常,更新失败!"); - } - if (!updateSuccess) throw new NoteServiceException("更新失败!"); - note.setTitle(null); - note.setKeywords(null); - note.setContent(null); - return note; - } - - @Override - public boolean deleteNote(Integer userId, Integer noteId) { - boolean deleteSuccess = false; - try { - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(Note::getId, noteId); - queryWrapper.eq(userId != null, Note::getUserId, userId); - deleteSuccess = this.remove(queryWrapper); - } catch (Exception e) { - log.error(e.toString()); - throw new NoteServiceException("出现异常,删除失败!"); - } - if (!deleteSuccess) throw new NoteServiceException("删除失败!"); - return true; - } - - @Override - public NotePageDTO pageNote(NotePageDTO notePageDTO, Integer requestUserId, boolean isGetSelf) { - try { - Page page = new Page<>(notePageDTO.getCurrent(), Math.min(notePageDTO.getSize(), maxPageSize)); - - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() - .select(Note::getId, Note::getTitle, Note::getIsPublic) - .eq(notePageDTO.getUserId() != null, Note::getUserId, notePageDTO.getUserId()) - .like(StrUtil.isNotBlank(notePageDTO.getSearchTitle()), Note::getTitle, notePageDTO.getSearchTitle()) - .like(StrUtil.isNotBlank(notePageDTO.getSearchContent()), Note::getContent, notePageDTO.getSearchContent()) - .like(StrUtil.isNotBlank(notePageDTO.getSearchKeyword()), Note::getKeywords, notePageDTO.getSearchKeyword()) - .and(!isGetSelf, qw -> qw - .eq(Note::getIsPublic, NoteIsPublic.YES) - .or(q -> q.ne(Note::getAccessCode, "")) - .or(requestUserId != null, q -> q.eq(Note::getUserId, requestUserId)) - ); - - List notes = this.page(page, queryWrapper).getRecords(); - Integer total = this.list(queryWrapper).size(); - notePageDTO.setList(notes); - notePageDTO.setTotal(total); - return notePageDTO; - } catch (Exception e) { - log.error(e.getMessage()); - throw new NoteServiceException("出现异常,分页失败!"); - } - } - - @Override - public NotePageDTO pageNote(NotePageDTO notePageDTO, Integer requestUserId) { - return pageNote(notePageDTO, requestUserId, false); - } -} diff --git a/src/main/java/com/xclhove/xnote/service/impl/UserServiceImpl.java b/src/main/java/com/xclhove/xnote/service/impl/UserServiceImpl.java deleted file mode 100644 index f614ac6..0000000 --- a/src/main/java/com/xclhove/xnote/service/impl/UserServiceImpl.java +++ /dev/null @@ -1,196 +0,0 @@ -package com.xclhove.xnote.service.impl; - -import cn.hutool.core.util.StrUtil; -import cn.hutool.crypto.SecureUtil; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.xclhove.xnote.constant.RedisKey; -import com.xclhove.xnote.constant.TreadLocalKey; -import com.xclhove.xnote.entity.table.User; -import com.xclhove.xnote.enums.entityattribute.UserStatus; -import com.xclhove.xnote.exception.UserServiceException; -import com.xclhove.xnote.exception.UserTokenException; -import com.xclhove.xnote.exception.VerificationCodeException; -import com.xclhove.xnote.mapper.UserMapper; -import com.xclhove.xnote.service.UserService; -import com.xclhove.xnote.tool.EmailTool; -import com.xclhove.xnote.tool.RedisTool; -import com.xclhove.xnote.tool.TokenTool; -import com.xclhove.xnote.util.*; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import java.util.concurrent.TimeUnit; - -/** - * @author xclhove - */ -@Service -@Slf4j -@RequiredArgsConstructor -public class UserServiceImpl extends ServiceImpl implements UserService { - private final String regex = "^[\\w.*_]{5,30}$"; - private final EmailTool emailTool; - private final RedisTool redisTool; - @Value("${xnote.debug.enable: false}") - private boolean isDebug; - private final TokenTool tokenTool; - - @Override - public boolean register(User user) { - boolean saveSuccess = false; - String account = user.getAccount(); - String password = user.getPassword(); - if (!account.matches(regex)) throw new UserServiceException("账号格式不正确!"); - if (!password.matches(regex)) throw new UserServiceException("密码格式不正确!"); - - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(User::getAccount, account); - User user2 = this.getOne(queryWrapper); - if (user2 != null) { - throw new UserServiceException("账号已存在!"); - } - - String encryptedPassword = EncryptUtil.encrypt(password, account, EncryptUtil.EncryptionAlgorithm.SHA256); - user.setPassword(encryptedPassword); - try { - saveSuccess = this.save(user); - } catch (Exception e) { - log.error(ExceptionUtil.getMessage(e)); - throw new UserServiceException("出现异常,注册失败!"); - } - if (!saveSuccess) throw new UserServiceException("注册失败,请重新注册!"); - return true; - } - - @Override - public String login(String account, String password) { - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - // 检查账号是否存在 - queryWrapper.eq(User::getAccount, account); - User user = this.getOne(queryWrapper); - if (user == null) { - throw new UserServiceException("账号不存在!"); - } - - // 检查密码是否正确 - String encryptPassword = EncryptUtil.encrypt(password, account, EncryptUtil.EncryptionAlgorithm.SHA256); - queryWrapper.eq(User::getPassword, encryptPassword); - user = this.getOne(queryWrapper); - if (user == null) { - throw new UserServiceException("密码错误!"); - } - - // 检查redis中是否已存在token - String token = tokenTool.get(user.getId()); - if (StrUtil.isNotBlank(token)) { - return token; - } - - // 生成新token - token = TokenUtil.generate(user.getId(), user.getPassword()); - tokenTool.set(user.getId(), token); - return token; - } - - @Override - public void logout(Integer userId) { - tokenTool.remove(userId); - String token = tokenTool.get(userId); - if (StrUtil.isNotBlank(token)) { - throw new UserServiceException("注销失败!"); - } - } - - @Override - public boolean updateInfo(User user) { - LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); - updateWrapper.eq(User::getId, user.getId()); - updateWrapper.set(!StrUtil.isBlank(user.getName()), User::getName, user.getName()); - updateWrapper.set(!StrUtil.isBlank(user.getEmail()), User::getEmail, user.getEmail()); - - String password = user.getPassword(); - if ((!StrUtil.isBlank(password)) && (!password.matches(regex))) throw new UserServiceException("密码格式不正确!"); - updateWrapper.set(!StrUtil.isBlank(password), - User::getPassword, - EncryptUtil.encrypt(password, user.getAccount(), EncryptUtil.EncryptionAlgorithm.SHA256) - ); - - boolean updateSuccess = false; - try { - updateSuccess = this.update(updateWrapper); - } catch (Exception exception) { - log.error(exception.getMessage()); - throw new UserServiceException("更新用户信息失败!"); - } - if (!updateSuccess) throw new UserServiceException("更新用户信息失败!"); - return true; - } - - @Override - public User queryById(Integer userId) { - if (!(userId > 0)) throw new UserServiceException("用户id错误!"); - User user = this.getById(userId); - if (user == null) throw new UserServiceException("用户不存在!"); - return user; - } - - @Override - public boolean banById(Integer userId) { - LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); - updateWrapper.eq(User::getId, userId) - .set(User::getStatus, UserStatus.BANED); - boolean banSuccess = false; - try { - banSuccess = this.update(updateWrapper); - } catch (Exception e) { - log.error(e.getMessage()); - throw new UserTokenException("出现异常,禁封失败!"); - } - if (!banSuccess) throw new UserTokenException("禁封失败!"); - return true; - } - - @Override - public void sendVerificationCode(String email) { - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(User::getEmail, email); - User user = this.getOne(queryWrapper); - if (user != null) { - throw new VerificationCodeException("该邮箱已被注册!"); - } - - int expirationDate = 1; - String verificationCode = VerificationCodeUtil.generateVerificationCode(4); - String generateVerificationCodeKey = VerificationCodeUtil.generateVerificationCodeKey(email); - redisTool.setValue(generateVerificationCodeKey, verificationCode, expirationDate, TimeUnit.MINUTES); - - String subject = "XNote验证码"; - String content = "XNote验证码:" + verificationCode + " ,有效期:" + expirationDate + "分钟。"; - try { - emailTool.sendMail(email, subject, content); - } catch (Exception e) { - log.error(ExceptionUtil.getMessage(e)); - throw new VerificationCodeException("验证码发送失败!"); - } - } - - @Override - public boolean verifyVerificationCode(String email, String verificationCode) { - if (isDebug) return true; - if (StrUtil.isBlank(verificationCode)) return false; - String verificationCodeKey = VerificationCodeUtil.generateVerificationCodeKey(email); - String code = redisTool.getValue(verificationCodeKey, String.class); - if (code == null) { - return false; - } - if (!code.equalsIgnoreCase(verificationCode)) { - return false; - } - redisTool.deleteValue(verificationCodeKey); - return true; - } -} diff --git a/src/main/java/com/xclhove/xnote/tool/ElasticsearchTool.java b/src/main/java/com/xclhove/xnote/tool/ElasticsearchTool.java new file mode 100644 index 0000000..fc9313b --- /dev/null +++ b/src/main/java/com/xclhove/xnote/tool/ElasticsearchTool.java @@ -0,0 +1,20 @@ +package com.xclhove.xnote.tool; + +import com.xclhove.xnote.util.ElasticsearchUtil; +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * @author xclhove + */ +@Component +@Data +public class ElasticsearchTool { + @Value("${spring.elasticsearch.uris}") + private String url; + + public ElasticsearchUtil.AnalysisResponse analyze(String ...text) { + return ElasticsearchUtil.analyze(url, text); + } +} diff --git a/src/main/java/com/xclhove/xnote/tool/EmailTool.java b/src/main/java/com/xclhove/xnote/tool/EmailTool.java index 59660da..f35640d 100644 --- a/src/main/java/com/xclhove/xnote/tool/EmailTool.java +++ b/src/main/java/com/xclhove/xnote/tool/EmailTool.java @@ -1,11 +1,14 @@ package com.xclhove.xnote.tool; -import com.xclhove.xnote.config.MailConfig; -import com.xclhove.xnote.util.EmailUtil; +import com.xclhove.xnote.config.JavaMailConfig; import lombok.RequiredArgsConstructor; +import org.springframework.mail.javamail.JavaMailSender; import org.springframework.stereotype.Component; +import javax.mail.Message; import javax.mail.MessagingException; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; /** * 邮件发送工具类 @@ -14,8 +17,9 @@ */ @Component @RequiredArgsConstructor -public class EmailTool { - private final MailConfig mailConfig; +public final class EmailTool { + private final JavaMailConfig javaMailConfig; + private final JavaMailSender javaMailSender; /** * 发送邮件 @@ -25,13 +29,11 @@ public class EmailTool { * @throws MessagingException 邮件发送异常 */ public void sendMail(String to, String subject, String content) throws MessagingException { - EmailUtil.sendEmail(mailConfig.getHost(), - mailConfig.getPort(), - mailConfig.getUsername(), - mailConfig.getPassword(), - mailConfig.getUsername(), - to, - subject, - content); + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + mimeMessage.setFrom(new InternetAddress(javaMailConfig.getUsername())); + mimeMessage.setRecipients(Message.RecipientType.TO, to); + mimeMessage.setSubject(subject); + mimeMessage.setText(content); + javaMailSender.send(mimeMessage); } } \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/tool/MinioTool.java b/src/main/java/com/xclhove/xnote/tool/MinioTool.java index 2c6b59f..980509a 100644 --- a/src/main/java/com/xclhove/xnote/tool/MinioTool.java +++ b/src/main/java/com/xclhove/xnote/tool/MinioTool.java @@ -1,26 +1,20 @@ package com.xclhove.xnote.tool; import cn.hutool.core.io.FastByteArrayOutputStream; -import cn.hutool.core.lang.UUID; import com.xclhove.xnote.config.MinioConfig; +import io.minio.Result; import io.minio.*; import io.minio.errors.ErrorResponseException; -import io.minio.http.Method; import io.minio.messages.Item; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletResponse; -import java.net.URLEncoder; import java.util.ArrayList; import java.util.List; /** - * Minio工具类 - * * @author xclhove */ @Slf4j @@ -30,143 +24,69 @@ public class MinioTool { private final MinioConfig minioConfig; private final MinioClient minioClient; - /** - * 查看存储bucket是否存在 - * - * @param bucketName 存储bucket名称 - * @return 是否存在 - */ - public boolean bucketExist(String bucketName) throws Exception { - boolean bucketExist = false; - bucketExist = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); - return bucketExist; - } /** * 查看存储bucket是否存在 - * - * @return 是否存在 */ public boolean bucketExist() throws Exception { - return bucketExist(minioConfig.getBucketName()); + boolean bucketExist = false; + String bucketName = minioConfig.getBucketName(); + BucketExistsArgs args = BucketExistsArgs.builder() + .bucket(bucketName) + .build(); + bucketExist = minioClient.bucketExists(args); + return bucketExist; } /** - * 创建存储bucket - * - * @param bucketName 存储bucket名称 - * @return Boolean + * 创建bucket */ - public boolean creatBucket(String bucketName) throws Exception { + public boolean createBucket() throws Exception { + String bucketName = minioConfig.getBucketName(); - minioClient.makeBucket(MakeBucketArgs.builder() + MakeBucketArgs args = MakeBucketArgs.builder() .bucket(bucketName) - .build()); - return bucketExist(bucketName); - - } - - /** - * 创建存储bucket - * - * @return Boolean - */ - public boolean creatBucket() throws Exception { - return creatBucket(minioConfig.getBucketName()); + .build(); + minioClient.makeBucket(args); + return bucketExist(); } /** - * 删除存储bucket - * - * @return 是否删除成功 + * 删除bucket */ - public boolean deleteBucket(String bucketName) throws Exception { - minioClient.removeBucket(RemoveBucketArgs.builder() + public boolean removeBucket() throws Exception { + String bucketName = minioConfig.getBucketName(); + + RemoveBucketArgs args = RemoveBucketArgs.builder() .bucket(bucketName) - .build()); - return !bucketExist(bucketName); + .build(); + minioClient.removeBucket(args); + return !bucketExist(); } /** * 文件上传 - * - * @param file 文件 - * @return 文件名 */ - public String upload(MultipartFile file) throws Exception { + public void upload(MultipartFile file, String fileNameInBucket) throws Exception { if (!bucketExist()) { - creatBucket(); + createBucket(); } - String originalFilename = file.getOriginalFilename(); - String fileSuffix = originalFilename.substring(originalFilename.lastIndexOf(".")); - String uuid = UUID.randomUUID().toString(); - String fileNameInBucket = uuid + fileSuffix; - PutObjectArgs objectArgs = PutObjectArgs.builder() + if (getFile(fileNameInBucket) != null) { + return; + } + + PutObjectArgs args = PutObjectArgs.builder() .bucket(minioConfig.getBucketName()) .object(fileNameInBucket) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build(); //文件名称相同会覆盖 - minioClient.putObject(objectArgs); - return fileNameInBucket; + minioClient.putObject(args); } - /** - * 预览文件 - * - * @param fileName 文件名 - * @return 文件预览地址 - */ - public String preview(String fileName) throws Exception { - - new GetPresignedObjectUrlArgs(); - GetPresignedObjectUrlArgs build = GetPresignedObjectUrlArgs - .builder() - .bucket(minioConfig.getBucketName()) - .object(fileName) - .method(Method.GET) - .build(); - String fileUrl = minioClient.getPresignedObjectUrl(build); - return fileUrl; - } - - /** - * 文件下载 - * - * @param fileName 文件名称 - * @param originName 文件原名称 - * @param servletResponse 响应 - */ - public void downloadFile(String fileName, String originName, HttpServletResponse servletResponse) throws Exception { - GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(minioConfig.getBucketName()) - .object(fileName).build(); - try (GetObjectResponse objectResponse = minioClient.getObject(objectArgs)) { - byte[] buf = new byte[1024]; - int len; - try (FastByteArrayOutputStream os = new FastByteArrayOutputStream()) { - while ((len = objectResponse.read(buf)) != -1) { - os.write(buf, 0, len); - } - os.flush(); - byte[] bytes = os.toByteArray(); - servletResponse.setCharacterEncoding("utf-8"); - // 设置强制下载不打开 - //servletResponse.setContentType("application/force-download"); - String documentFileName = originName.substring(0, originName.lastIndexOf(".")); - String documentFileType = fileName.substring(fileName.lastIndexOf(".")); - String finalFileName = documentFileName + documentFileType; - finalFileName = finalFileName.replaceAll("\\s+", "_"); - finalFileName = URLEncoder.encode(finalFileName, "UTF-8"); - servletResponse.addHeader("Content-Disposition", "attachment;fileName=" + finalFileName); - servletResponse.addHeader("file-name", finalFileName); - servletResponse.setHeader("Access-Control-Expose-Headers", "file-name"); - try (ServletOutputStream stream = servletResponse.getOutputStream()) { - stream.write(bytes); - stream.flush(); - } - } - } + public String getFileUrl(String fileName) { + return String.join("/", minioConfig.getRemoteEndpoint(), minioConfig.getBucketName(), fileName); } /** @@ -179,14 +99,14 @@ public byte[] downloadFile(String fileName) throws Exception { GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(minioConfig.getBucketName()) .object(fileName).build(); try (GetObjectResponse objectResponse = minioClient.getObject(objectArgs)) { - byte[] buf = new byte[1024]; - int len; - try (FastByteArrayOutputStream os = new FastByteArrayOutputStream()) { - while ((len = objectResponse.read(buf)) != -1) { - os.write(buf, 0, len); + byte[] buffer = new byte[1024]; + int length; + try (FastByteArrayOutputStream outputStream = new FastByteArrayOutputStream()) { + while ((length = objectResponse.read(buffer)) != -1) { + outputStream.write(buffer, 0, length); } - os.flush(); - byte[] bytes = os.toByteArray(); + outputStream.flush(); + byte[] bytes = outputStream.toByteArray(); return bytes; } } @@ -198,11 +118,10 @@ public byte[] downloadFile(String fileName) throws Exception { * @return 文件对象列表 */ public List listFiles() throws Exception { - Iterable> results = minioClient.listObjects( - ListObjectsArgs.builder() - .bucket(minioConfig.getBucketName()) - .build() - ); + ListObjectsArgs args = ListObjectsArgs.builder() + .bucket(minioConfig.getBucketName()) + .build(); + Iterable> results = minioClient.listObjects(args); List items = new ArrayList<>(); for (Result result : results) { @@ -220,13 +139,11 @@ public List listFiles() throws Exception { */ public StatObjectResponse getFile(String fileName) throws Exception { try { - StatObjectResponse file = minioClient.statObject( - StatObjectArgs - .builder() - .bucket(minioConfig.getBucketName()).object(fileName) - .build() - ); - + StatObjectArgs args = StatObjectArgs.builder() + .bucket(minioConfig.getBucketName()) + .object(fileName) + .build(); + StatObjectResponse file = minioClient.statObject(args); return file; } catch (ErrorResponseException e) { if (e.getMessage().contains("Object does not exist")) { @@ -236,27 +153,13 @@ public StatObjectResponse getFile(String fileName) throws Exception { } } - /** - * 通过文件路径获取文件对象 - * - * @param filePath 文件路径 - * @return 文件对象 - */ - public StatObjectResponse getFileByFilePath(String filePath) throws Exception { - - String fileName = filePath.substring(filePath.lastIndexOf("/")); - StatObjectResponse file = getFile(fileName); - return file; - - } - /** * 通过文件名删除文件 * * @param fileName 文件名 * @return 是否删除成功 */ - public boolean deleteFileByFileName(String fileName) throws Exception { + public boolean deleteFile(String fileName) throws Exception { minioClient.removeObject( RemoveObjectArgs diff --git a/src/main/java/com/xclhove/xnote/tool/RedisTool.java b/src/main/java/com/xclhove/xnote/tool/RedisTool.java index 511e404..2729df9 100644 --- a/src/main/java/com/xclhove/xnote/tool/RedisTool.java +++ b/src/main/java/com/xclhove/xnote/tool/RedisTool.java @@ -1,67 +1,148 @@ package com.xclhove.xnote.tool; -import com.alibaba.fastjson2.JSON; import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.RedisConnectionFailureException; -import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; -import java.util.Arrays; import java.util.concurrent.TimeUnit; /** - * redis工具类 - * * @author xclhove */ @Component @RequiredArgsConstructor -public class RedisTool { - private final RedisTemplate redisTemplate; +public final class RedisTool { + private final StringRedisTemplate stringRedisTemplate; - public boolean connected() { - try { - redisTemplate.opsForValue().get("*"); - return true; - } catch (RedisConnectionFailureException e) { - return false; - } - } - - public void setValue(String key, Object value) { - redisTemplate.opsForValue().set(key, JSON.toJSONString(value)); - } - - public void setValue(String key, Object value, long timeout, TimeUnit unit) { - redisTemplate.opsForValue().set(key, JSON.toJSONString(value), timeout, unit); - } - - public Object getValue(String key) { - String value = redisTemplate.opsForValue().get(key); - return JSON.parse(value); + @FunctionalInterface + public interface Getter { + /** + * 从数据库中查询数据 + */ + R getFormDatabase(); } - public T getValue(String key, Class resultType) { - return (T) getValue(key); + @FunctionalInterface + public interface Serializer { + /** + * 序列化以便存到 redis + */ + R serialize(T dataFormDatabase); } - public Boolean deleteValue(String key) { - return redisTemplate.delete(key); + @FunctionalInterface + public interface DeSerializer { + /** + * 反序列化 redis 中存储的数据 + */ + R deserialize(T dataFormRedis); } - public void setHashValue(String key, Object hashKey, Object value) { - redisTemplate.opsForHash().put(key, hashKey.toString(), value); + /** + * 防缓存穿透 + */ + public R getUseStringAntiCachePassThrough( + String redisKey, + Getter getter, + Serializer serializer, + DeSerializer deSerializer + ) { + String valueInRedis = stringRedisTemplate.opsForValue().get(redisKey); + if (valueInRedis == null) { + R objectInDatabase = getter.getFormDatabase(); + + String value = ""; + if (objectInDatabase != null) { + value = serializer.serialize(objectInDatabase); + } + stringRedisTemplate.opsForValue().set(redisKey, value); + + return objectInDatabase; + } + + if (valueInRedis.isEmpty()) { + return null; + } + + R objectInRedis = deSerializer.deserialize(valueInRedis); + return objectInRedis; } - public Object getHashValue(String key, Object hashKey) { - return redisTemplate.opsForHash().get(key, hashKey.toString()); + /** + * 防缓存穿透 + */ + public R getUseStringAntiCachePassThrough( + String redisKey, + Getter getter, + Serializer serializer, + DeSerializer deSerializer, + long timeout, + TimeUnit timeUnit + ) { + String valueInRedis = stringRedisTemplate.opsForValue().get(redisKey); + if (valueInRedis == null) { + R objectInDatabase = getter.getFormDatabase(); + + String value = ""; + if (objectInDatabase != null) { + value = serializer.serialize(objectInDatabase); + } + stringRedisTemplate.opsForValue().set(redisKey, value, timeout, timeUnit); + + return objectInDatabase; + } + + if (valueInRedis.isEmpty()) { + return null; + } + + R objectInRedis = deSerializer.deserialize(valueInRedis); + return objectInRedis; } - public T getHashValue(String key, Object hashKey, Class resultType) { - return (T) getHashValue(key, hashKey.toString()); + public R getUseString( + String redisKey, + Getter getter, + Serializer serializer, + DeSerializer deSerializer + ) { + String valueInRedis = stringRedisTemplate.opsForValue().get(redisKey); + if (valueInRedis == null || valueInRedis.isEmpty()) { + R objectInDatabase = getter.getFormDatabase(); + + if (objectInDatabase != null) { + String value = serializer.serialize(objectInDatabase); + stringRedisTemplate.opsForValue().set(redisKey, value); + } + + return objectInDatabase; + } + + R objectInRedis = deSerializer.deserialize(valueInRedis); + return objectInRedis; } - public Long deleteHashValue(String key, Object... hashKeys) { - return redisTemplate.opsForHash().delete(key, Arrays.toString(hashKeys)); + public R getUseString( + String redisKey, + Getter getter, + Serializer serializer, + DeSerializer deSerializer, + long timeout, + TimeUnit timeUnit + ) { + String valueInRedis = stringRedisTemplate.opsForValue().get(redisKey); + if (valueInRedis == null || valueInRedis.isEmpty()) { + R objectInDatabase = getter.getFormDatabase(); + + if (objectInDatabase != null) { + String value = serializer.serialize(objectInDatabase); + stringRedisTemplate.opsForValue().set(redisKey, value, timeout, timeUnit); + } + + return objectInDatabase; + } + + R objectInRedis = deSerializer.deserialize(valueInRedis); + return objectInRedis; } -} \ No newline at end of file +} diff --git a/src/main/java/com/xclhove/xnote/util/Result.java b/src/main/java/com/xclhove/xnote/tool/Result.java similarity index 56% rename from src/main/java/com/xclhove/xnote/util/Result.java rename to src/main/java/com/xclhove/xnote/tool/Result.java index 987d4bc..a43c27f 100644 --- a/src/main/java/com/xclhove/xnote/util/Result.java +++ b/src/main/java/com/xclhove/xnote/tool/Result.java @@ -1,8 +1,6 @@ -package com.xclhove.xnote.util; +package com.xclhove.xnote.tool; -import com.xclhove.xnote.enums.result.ResultType; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; +import com.xclhove.xnote.enums.ResultType; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -13,13 +11,9 @@ @Data @AllArgsConstructor @NoArgsConstructor -@ApiModel(value = "结果") public class Result { - @ApiModelProperty(value = "状态码", example = "200") private int status; - @ApiModelProperty(value = "信息", example = "请求成功!") private String message; - @ApiModelProperty(value = "数据") private T data; public static Result success() { @@ -38,19 +32,19 @@ public static Result success(int status, String message, T data) { return new Result<>(status, message, data); } - public static Result error() { - return new Result<>(ResultType.ERROR.getStatus(), ResultType.ERROR.getMessage(), null); + public static Result fail() { + return new Result<>(ResultType.FAIL.getStatus(), ResultType.FAIL.getMessage(), null); } - public static Result error(String message) { - return new Result<>(ResultType.ERROR.getStatus(), message, null); + public static Result fail(String message) { + return new Result<>(ResultType.FAIL.getStatus(), message, null); } - public static Result error(int status, String message) { + public static Result fail(int status, String message) { return new Result<>(status, message, null); } - public static Result error(int status, String message, T data) { + public static Result fail(int status, String message, T data) { return new Result<>(status, message, data); } } diff --git a/src/main/java/com/xclhove/xnote/tool/ThreadLocalTool.java b/src/main/java/com/xclhove/xnote/tool/ThreadLocalTool.java new file mode 100644 index 0000000..3e41b61 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/tool/ThreadLocalTool.java @@ -0,0 +1,44 @@ +package com.xclhove.xnote.tool; + +import com.xclhove.xnote.constant.TreadLocalKey; +import com.xclhove.xnote.pojo.table.User; +import com.xclhove.xnote.util.ThreadLocalUtil; + +/** + * @author xclhove + */ +public final class ThreadLocalTool { + private ThreadLocalTool() {} + + /** + * 从ThreadLocal获取当前用户 + */ + public static User getUser() { + User user = ThreadLocalUtil.get(TreadLocalKey.USER, User.class); + return user; + } + + public static void setUser(User user) { + ThreadLocalUtil.set(TreadLocalKey.USER, user); + } + + /** + * 从ThreadLocal获取deviceId + */ + public static String getDeviceId() { + String deviceId = ThreadLocalUtil.get(TreadLocalKey.DEVICE_ID, String.class); + return deviceId; + } + + public static void setDeviceId(String deviceId) { + ThreadLocalUtil.set(TreadLocalKey.DEVICE_ID, deviceId); + } + + public static void setClientIp(String clientIpAddress) { + ThreadLocalUtil.set(TreadLocalKey.CLIENT_IP, clientIpAddress); + } + + public static String getClientIpAddress() { + return ThreadLocalUtil.get(TreadLocalKey.CLIENT_IP, String.class); + } +} diff --git a/src/main/java/com/xclhove/xnote/tool/TokenTool.java b/src/main/java/com/xclhove/xnote/tool/TokenTool.java deleted file mode 100644 index 1cca431..0000000 --- a/src/main/java/com/xclhove/xnote/tool/TokenTool.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.xclhove.xnote.tool; - -import com.xclhove.xnote.constant.RedisKey; -import com.xclhove.xnote.constant.TreadLocalKey; -import com.xclhove.xnote.util.ThreadLocalUtil; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.concurrent.TimeUnit; - -/** - * @author xclhove - */ -@Component -@RequiredArgsConstructor -public class TokenTool { - private final RedisTool redisTool; - - public String get(Integer userId) { - String key = generateTokenKey(userId); - return redisTool.getValue(key, String.class); - } - - public void set(Integer userId, String token) { - String key = generateTokenKey(userId); - redisTool.setValue(key, token, 24, TimeUnit.HOURS); - } - - public void remove(Integer userId) { - String key = generateTokenKey(userId); - redisTool.deleteValue(key); - } - - private String generateTokenKey(Integer userId) { - String deviceId = ThreadLocalUtil.get(TreadLocalKey.DEVICE_ID, String.class); - String key = RedisKey.USER_TOKEN + ":" + userId + ":" + deviceId; - return key; - } -} diff --git a/src/main/java/com/xclhove/xnote/tool/UserTokenTool.java b/src/main/java/com/xclhove/xnote/tool/UserTokenTool.java new file mode 100644 index 0000000..424d6a9 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/tool/UserTokenTool.java @@ -0,0 +1,39 @@ +package com.xclhove.xnote.tool; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; + +import java.time.Duration; +import java.util.Date; + +/** + * @author xclhove + */ +public class UserTokenTool { + + public static String generate(String encryptPassword, Integer userId) { + return JWT.create() + .withAudience(String.valueOf(userId)) + //2小时后token过期 + .withExpiresAt(new Date(System.currentTimeMillis() + Duration.ofHours(2).toMillis())) + // 以 encryptPassword 作为签名密钥 + .sign(Algorithm.HMAC256(encryptPassword)); + } + + public static Integer getUserId(String token) { + try { + return Integer.parseInt(JWT.decode(token).getAudience().get(0)); + } catch (Exception exception) { + return null; + } + } + + public static boolean valid(String token, String encryptPassword) { + try { + JWT.require(Algorithm.HMAC256(encryptPassword)).build().verify(token); + return true; + } catch (Exception e) { + return false; + } + } +} diff --git a/src/main/java/com/xclhove/xnote/util/ByteSizeUtil.java b/src/main/java/com/xclhove/xnote/util/ByteSizeUtil.java new file mode 100644 index 0000000..202acf6 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/util/ByteSizeUtil.java @@ -0,0 +1,35 @@ +package com.xclhove.xnote.util; + +import java.util.HashMap; +import java.util.Map; + +public class ByteSizeUtil { + static final Map UNIT_TO_LEVEL; + static final Map LEVEL_TO_UNIT; + static final short MIN_LEVEL = 0; + static { + short index = MIN_LEVEL; + UNIT_TO_LEVEL = new HashMap<>(); + UNIT_TO_LEVEL.put("B", index++); + UNIT_TO_LEVEL.put("KB", index++); + UNIT_TO_LEVEL.put("MB", index++); + UNIT_TO_LEVEL.put("GB", index++); + UNIT_TO_LEVEL.put("TB", index++); + UNIT_TO_LEVEL.put("PB", index++); + + LEVEL_TO_UNIT = new HashMap<>(UNIT_TO_LEVEL.size()); + UNIT_TO_LEVEL.forEach((key, value) -> { + LEVEL_TO_UNIT.put(value, key); + }); + } + + public static String parseSizeWithUnit(long size) { + short sizeLevel = MIN_LEVEL; + float sizeFloat = size; + while (sizeFloat > 1024) { + sizeFloat /= 1024f; + sizeLevel++; + } + return String.format("%.2f%s", sizeFloat, LEVEL_TO_UNIT.get(sizeLevel)); + } +} diff --git a/src/main/java/com/xclhove/xnote/util/ElasticsearchUtil.java b/src/main/java/com/xclhove/xnote/util/ElasticsearchUtil.java new file mode 100644 index 0000000..abdbcb5 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/util/ElasticsearchUtil.java @@ -0,0 +1,77 @@ +package com.xclhove.xnote.util; + +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import com.alibaba.fastjson2.JSON; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.util.Arrays; +import java.util.List; + +/** + * @author xclhove + */ +public final class ElasticsearchUtil { + private ElasticsearchUtil() { + } + + public static AnalysisResponse analyze(String url, String ...text) { + AnalysisRequest analysisRequest = new AnalysisRequest(); + analysisRequest.setAnalyzer("ik_max_word"); + analysisRequest.setText(Arrays.asList(text)); + String jsonString = JSON.toJSONString(analysisRequest); + + try (HttpResponse response = HttpRequest + .get(url + "/_analyze") + .body(jsonString) + .header("Content-Type", "application/json") + .execute() + ) { + String body = response.body(); + return JSON.parseObject(body, AnalysisResponse.class); + } + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + @Accessors(chain = true) + public static class AnalysisRequest { + private String analyzer = "standard"; + private List text; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + @Accessors(chain = true) + public static class AnalysisResponse { + @JsonProperty("tokens") + private List tokens; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + @Accessors(chain = true) + public static class Token { + @JsonProperty("token") + private String token; + @JsonProperty("start_offset") + private Integer startOffset; + @JsonProperty("end_offset") + private Integer endOffset; + @JsonProperty("type") + private String type; + @JsonProperty("position") + private Integer position; + + public boolean isNotSingleChar() { + return token.length() != 1; + } + } +} diff --git a/src/main/java/com/xclhove/xnote/util/EmailUtil.java b/src/main/java/com/xclhove/xnote/util/EmailUtil.java index e69fec6..91fd292 100644 --- a/src/main/java/com/xclhove/xnote/util/EmailUtil.java +++ b/src/main/java/com/xclhove/xnote/util/EmailUtil.java @@ -19,10 +19,10 @@ public class EmailUtil { * * @param host 主机名称或IP地址,如:smtp.qq.com * @param port 端口号,如:465,587,25 - * @param username 邮箱账号,如:example@qq.com + * @param username 邮箱账号,如:sender@qq.com * @param password 邮箱密码 - * @param from 发送方邮箱地址,如:example@qq.com - * @param to 接收方邮箱地址,如:example@qq.com + * @param from 发送方邮箱地址,如:sender@qq.com + * @param to 接收方邮箱地址,如:reciver@qq.com * @param subject 邮件主题 * @param text 邮件正文 * @throws MessagingException 异常信息 @@ -43,8 +43,7 @@ public static void sendEmail(String host, int port, String username, String pass message.setFrom(new InternetAddress(from)); message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to)); message.setSubject(subject); - String finalText = "\n------------------分割线------------------\n"; - message.setText(text + finalText); + message.setText(text); // 发送邮件 Transport transport = session.getTransport("smtp"); diff --git a/src/main/java/com/xclhove/xnote/util/Md5Util.java b/src/main/java/com/xclhove/xnote/util/Md5Util.java new file mode 100644 index 0000000..a28f8f6 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/util/Md5Util.java @@ -0,0 +1,36 @@ +package com.xclhove.xnote.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * @author xclhove + */ +public final class Md5Util { + private static MessageDigest messageDigest; + + private Md5Util() { + } + + private static MessageDigest getInstance() throws NoSuchAlgorithmException { + if (messageDigest != null) { + return messageDigest; + } + synchronized (Md5Util.class) { + if (messageDigest == null) { + messageDigest = MessageDigest.getInstance("MD5"); + } + return messageDigest; + } + } + + public static String getMd5(byte[] bytes) throws Exception { + MessageDigest messageDigest = getInstance(); + bytes = messageDigest.digest(bytes); + StringBuilder md5 = new StringBuilder(); + for (byte b : bytes) { + md5.append(String.format("%02x", b)); + } + return md5.toString(); + } +} diff --git a/src/main/java/com/xclhove/xnote/util/PackageUtil.java b/src/main/java/com/xclhove/xnote/util/PackageUtil.java new file mode 100644 index 0000000..7920180 --- /dev/null +++ b/src/main/java/com/xclhove/xnote/util/PackageUtil.java @@ -0,0 +1,16 @@ +package com.xclhove.xnote.util; + +/** + * @author xclhove + */ +public class PackageUtil { + public static String getBasePackage() { + String currentPackage = PackageUtil.class.getPackage().getName(); + String parentPackage = currentPackage.substring(0, currentPackage.lastIndexOf(".")); + return parentPackage; + } + + public static String join(String... strings) { + return String.join(".", strings); + } +} \ No newline at end of file diff --git a/src/main/java/com/xclhove/xnote/util/RequestUtil.java b/src/main/java/com/xclhove/xnote/util/RequestUtil.java index 71a20fa..b021734 100644 --- a/src/main/java/com/xclhove/xnote/util/RequestUtil.java +++ b/src/main/java/com/xclhove/xnote/util/RequestUtil.java @@ -9,28 +9,38 @@ */ public class RequestUtil { /** - * 获取ip地址 - * - * @param request 请求 - * @return ip地址 + * 获取客户端ip地址 */ - public static String getIpAddr(HttpServletRequest request) { - String ip = request.getHeader("x-forwarded-for"); - if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("Proxy-Client-IP"); + public static String getClientIpAddress(HttpServletRequest request) { + String ipAddress = request.getHeader("x-forwarded-for"); + if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) { + ipAddress = request.getHeader("Proxy-Client-IP"); } - if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("WL-Proxy-Client-IP"); + if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) { + ipAddress = request.getHeader("WL-Proxy-Client-IP"); } - if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("HTTP_CLIENT_IP"); + if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) { + ipAddress = request.getHeader("HTTP_CLIENT_IP"); } - if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("X-Real-IP"); + if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) { + ipAddress = request.getHeader("X-Real-IP"); } - if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getRemoteAddr(); + if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) { + ipAddress = request.getRemoteAddr(); } - return ip; + if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) { + throw new RuntimeException("获取IP地址失败"); + } + return ipAddress; + } + + /** + * 获取当前路径 + */ + public static String getCurrentPath(HttpServletRequest request) { + String requestUri = request.getRequestURI(); + String contextPath = request.getContextPath(); + String currentPath = requestUri.substring(contextPath.length()); + return currentPath; } } diff --git a/src/main/java/com/xclhove/xnote/util/SubclassFinder.java b/src/main/java/com/xclhove/xnote/util/SubclassFinder.java index a9ab344..dbaed36 100644 --- a/src/main/java/com/xclhove/xnote/util/SubclassFinder.java +++ b/src/main/java/com/xclhove/xnote/util/SubclassFinder.java @@ -12,10 +12,16 @@ */ public class SubclassFinder { + /** + * 查找子类 + */ public static Set findSubclasses(Class clazz) { - return findSubclasses(clazz, "com.xclhove.xnote"); + return findSubclasses(clazz, PackageUtil.getBasePackage()); } + /** + * 查找子类 + */ public static Set findSubclasses(Class clazz, String basePackage) { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); provider.addIncludeFilter(new AssignableTypeFilter(clazz)); diff --git a/src/main/java/com/xclhove/xnote/util/ThreadLocalUtil.java b/src/main/java/com/xclhove/xnote/util/ThreadLocalUtil.java index dd58593..b4fec94 100644 --- a/src/main/java/com/xclhove/xnote/util/ThreadLocalUtil.java +++ b/src/main/java/com/xclhove/xnote/util/ThreadLocalUtil.java @@ -11,6 +11,15 @@ public class ThreadLocalUtil { private final static ThreadLocal> THREAD_LOCAL = new ThreadLocal<>(); + private static Map getDataMap() { + Map dataMap = THREAD_LOCAL.get(); + if (dataMap == null) { + dataMap = new HashMap<>(); + } + + return dataMap; + } + /** * 获取数据 * @@ -18,7 +27,7 @@ public class ThreadLocalUtil { * @return 数据值 */ public static Object get(String name) { - Map dataMap = THREAD_LOCAL.get(); + Map dataMap = getDataMap(); return dataMap.get(name); } @@ -30,7 +39,7 @@ public static Object get(String name) { * @param */ public static T get(String name, Class resultType) { - Map dataMap = THREAD_LOCAL.get(); + Map dataMap = getDataMap(); return (T) dataMap.get(name); } @@ -40,8 +49,8 @@ public static T get(String name, Class resultType) { * @param value 数据值 */ public static void set(String name, Object value) { - Map dataMap = THREAD_LOCAL.get(); - if (dataMap == null) dataMap = new HashMap<>(); + Map dataMap = getDataMap(); + dataMap.put(name, value); THREAD_LOCAL.set(dataMap); } diff --git a/src/main/java/com/xclhove/xnote/util/TokenUtil.java b/src/main/java/com/xclhove/xnote/util/TokenUtil.java deleted file mode 100644 index acd686f..0000000 --- a/src/main/java/com/xclhove/xnote/util/TokenUtil.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.xclhove.xnote.util; - -import cn.hutool.core.date.DateUtil; -import com.auth0.jwt.JWT; -import com.auth0.jwt.JWTVerifier; -import com.auth0.jwt.algorithms.Algorithm; - -import java.util.Date; -import java.util.List; - -/** - * @author xclhove - */ -public class TokenUtil { - - - /** - * 生成token - * @param id Id - * @param secret 签名密钥 - * @return token - */ - public static String generate(int id, String secret) { - return JWT.create().withAudience(String.valueOf(id)) // 将 user id 保存到 token 里面 - .withExpiresAt(DateUtil.offsetHour(new Date(), 24)) //2小时后token过期 - .sign(Algorithm.HMAC256(secret)); // 以 secret 作为签名密钥 - } - - /** - * 从token中提取id - * @param token token - * @return 用户id - */ - public static Integer getId(String token) { - try { - List audience = JWT.decode(token).getAudience(); - int userId = Integer.parseInt(audience.get(0)); - return userId; - } catch (Exception exception) { - return null; - } - } - - /** - * 验证token - * @param token token - * @param secret 签名密钥 - * @return 验证是否通过 - */ - public static boolean validate(String token, String secret) { - try { - Algorithm algorithm = Algorithm.HMAC256(secret); - JWTVerifier verifier = JWT.require(algorithm).build(); - verifier.verify(token); - return true; // token验证通过 - } catch (Exception e) { - return false; - } - } -} diff --git a/src/main/java/com/xclhove/xnote/util/UuidUtil.java b/src/main/java/com/xclhove/xnote/util/UuidUtil.java new file mode 100644 index 0000000..497896c --- /dev/null +++ b/src/main/java/com/xclhove/xnote/util/UuidUtil.java @@ -0,0 +1,27 @@ +package com.xclhove.xnote.util; + +public class UuidUtil { + private final static String UUID_REGEX = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"; + private final static String SIMPLE_UUID_REGEX = "^[0-9a-f]{8}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{12}$"; + private final static int UUID_LENGTH = 36; + private final static int SIMPLE_UUID_LENGTH = 32; + public static boolean isUuid(String uuid) { + if (uuid == null) { + return false; + } + if (uuid.length() != UUID_LENGTH) { + return false; + } + return uuid.matches(UUID_REGEX); + } + + public static boolean isSimpleUuid(String simpleUuid) { + if (simpleUuid == null) { + return false; + } + if (simpleUuid.length() != SIMPLE_UUID_LENGTH) { + return false; + } + return simpleUuid.matches(SIMPLE_UUID_REGEX); + } +} diff --git a/src/main/java/com/xclhove/xnote/util/ValidateCodeImageUtil.java b/src/main/java/com/xclhove/xnote/util/ValidateCodeImageUtil.java new file mode 100644 index 0000000..08b46cc --- /dev/null +++ b/src/main/java/com/xclhove/xnote/util/ValidateCodeImageUtil.java @@ -0,0 +1,111 @@ +package com.xclhove.xnote.util; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Base64; +import java.util.Random; + +/** + * @author xclhove + */ +public class ValidateCodeImageUtil { + private static final Random RANDOM = new Random(); + /** + * 验证码的宽 + */ + private static final int DEFAULT_WIDTH = 165; + /** + * 验证码的高 + */ + private static final int DEFAULT_HEIGHT = 45; + /** + * 验证码中夹杂的干扰线数量 + */ + private static final int DEFAULT_LINE_SIZE = 30; + /** + * 字体 + */ + private static final Font DEFAULT_FONT = new Font("Times New Roman", Font.ROMAN_BASELINE, 40); + /** + * 图片的contentType + */ + public static final String CONTENT_TYPE = "image/png"; + + /** + * 随机颜色 + */ + private static Color generateRandomColor(int foregroundColor, int backgroundColor) { + foregroundColor = Math.min(foregroundColor, 255); + backgroundColor = Math.min(backgroundColor, 255); + + int r = foregroundColor + RANDOM.nextInt(backgroundColor - foregroundColor - 16); + int g = foregroundColor + RANDOM.nextInt(backgroundColor - foregroundColor - 14); + int b = foregroundColor + RANDOM.nextInt(backgroundColor - foregroundColor - 12); + + return new Color(r, g, b); + } + + /** + * 干扰线的绘制 + */ + private static void drawLine(Graphics graphics) { + int x = RANDOM.nextInt(DEFAULT_WIDTH); + int y = RANDOM.nextInt(DEFAULT_HEIGHT); + int xl = RANDOM.nextInt(20); + int yl = RANDOM.nextInt(10); + graphics.drawLine(x, y, x + xl, y + yl); + + } + + /** + * 字符串的绘制 + */ + private static void drawChar(Graphics graphics, char charOfCode, int charIndex) { + graphics.setFont(DEFAULT_FONT); + graphics.setColor(generateRandomColor(108, 190)); + graphics.translate(RANDOM.nextInt(3), RANDOM.nextInt(6)); + graphics.drawString(String.valueOf(charOfCode), 40 * charIndex + 10, 25); + } + + + /** + * 生成随机图片 + */ + public static BufferedImage generateImage(String code){ + BufferedImage bufferedImage = new BufferedImage(DEFAULT_WIDTH, DEFAULT_HEIGHT, BufferedImage.TYPE_INT_BGR); + Graphics imageGraphics = bufferedImage.getGraphics(); + imageGraphics.fillRect(0, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT); + imageGraphics.setColor(generateRandomColor(105, 189)); + imageGraphics.setFont(DEFAULT_FONT); + + // 干扰线 + for (int i = 0; i < DEFAULT_LINE_SIZE; i++) { + drawLine(imageGraphics); + } + // 依次绘制字符 + for (int i = 0; i < code.length(); i++) { + drawChar(imageGraphics, code.charAt(i), i); + } + imageGraphics.dispose(); + + return bufferedImage; + } + + + /** + * 生成随机图片的base64编码字符串 + */ + public static String generateImageBase64(String code) throws IOException { + BufferedImage image = generateImage(code); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ImageIO.write(image, "PNG", byteArrayOutputStream); + + byte[] bytes = byteArrayOutputStream.toByteArray(); + Base64.Encoder encoder = Base64.getEncoder(); + String imageBase64 = "data:" + CONTENT_TYPE + ";base64," + encoder.encodeToString(bytes); + return imageBase64; + } +} diff --git a/src/main/java/com/xclhove/xnote/util/VariableNameUtil.java b/src/main/java/com/xclhove/xnote/util/VariableNameUtil.java new file mode 100644 index 0000000..5f7152c --- /dev/null +++ b/src/main/java/com/xclhove/xnote/util/VariableNameUtil.java @@ -0,0 +1,33 @@ +package com.xclhove.xnote.util; + +/** + * @author xclhove + */ +public final class VariableNameUtil { + private VariableNameUtil() {} + + public static String toUnderlineName(String camelCaseName) { + if (camelCaseName == null || camelCaseName.isEmpty()) { + return camelCaseName; + } + + StringBuilder result = new StringBuilder(); + boolean isFirstCharacter = true; + + for (char ch : camelCaseName.toCharArray()) { + if (Character.isUpperCase(ch)) { + if (!isFirstCharacter) { + result.append('_'); + } + result.append(Character.toLowerCase(ch)); + isFirstCharacter = false; + } else { + result.append(ch); + isFirstCharacter = false; + } + } + + return result.toString(); + } + +} diff --git a/src/main/java/com/xclhove/xnote/util/VerificationCodeUtil.java b/src/main/java/com/xclhove/xnote/util/VerificationCodeUtil.java index a1102e5..0def9dd 100644 --- a/src/main/java/com/xclhove/xnote/util/VerificationCodeUtil.java +++ b/src/main/java/com/xclhove/xnote/util/VerificationCodeUtil.java @@ -1,42 +1,52 @@ package com.xclhove.xnote.util; -import com.xclhove.xnote.constant.RedisKey; - /** * 验证码工具类 * * @author xclhove */ public class VerificationCodeUtil { + private final static int NUMBER_ONLY_LENGTH = 10; /** - * 生成一个由字母和数字组成的随机验证码 + * 生成一个随机验证码 * * @param codeLength 验证码的长度 + * @param isNumberOnly 是否只包含数字 * @return 生成的验证码 */ - public static String generateVerificationCode(int codeLength) { - char[] characters = new char[62]; + public static String generate(int codeLength, boolean isNumberOnly) { + char[] characters = new char[isNumberOnly ? NUMBER_ONLY_LENGTH : 62]; int index = 0; - for (int i = 0; i <= 9; i++) { + for (int i = 0; i < NUMBER_ONLY_LENGTH; i++) { characters[index++] = (char) ('0' + i); } - for (int i = 0; i < 26; i++) { - characters[index++] = (char) ('a' + i); - } - for (int i = 0; i < 26; i++) { - characters[index++] = (char) ('A' + i); + + if (!isNumberOnly) { + for (int i = 0; i < 26; i++) { + characters[index++] = (char) ('a' + i); + } + for (int i = 0; i < 26; i++) { + characters[index++] = (char) ('A' + i); + } } StringBuilder verificationCode = new StringBuilder(codeLength); for (int i = 0; i < codeLength; i++) { - verificationCode.append(characters[(int) (Math.random() * 62)]); + int randomIndex = ((int) (Math.random() * 100)) % characters.length; + verificationCode.append(characters[randomIndex]); } return verificationCode.toString(); } - public static String generateVerificationCodeKey(String email) { - return RedisKey.VERIFICATION_CODE_OF + email; + /** + * 生成一个由字母和数字组成的随机验证码 + * + * @param codeLength 验证码的长度 + * @return 生成的验证码 + */ + public static String generate(int codeLength) { + return generate(codeLength, false); } } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..2309e18 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,50 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/xnote?rewriteBatchedStatements=true + username: root + password: 123456 + redis: + host: localhost + port: 6379 + password: 123456 + mail: + host: smtp.qq.com + port: 587 + username: ${EMAIL_ADDRESS} + password: ${EMAIL_PASSWORD} + elasticsearch: + uris: http://localhost:9200 + username: xclhove + password: 123456 + +minio: + endpoint: http://localhost:9101 + remoteEndpoint: http://localhost:9101 + bucketName: xnote + accessKey: admin + secretKey: 12345678 + +xnote: + debug: + enable-mock-send-verification-code: true + enable-verify-verification-code: true + runner: + enable-check-redis-status: true + enable-check-minio-status: true + import-data-from-database-to-elastic-search: true + update-image-size: false + send-startup-email: false + create-default-note-type-for-user-of-none-type: false + replace-image-url-in-note: false + interceptor: + ip: + disable: true + max-frequency-per-minute: 120 + device: + disable: true + max-frequency-per-minute: 120 + image: + allow-size-of-byte: 10485760 + search: + height-light-content-max-length: 400 \ No newline at end of file diff --git a/src/main/resources/application-flyway.yml b/src/main/resources/application-flyway.yml new file mode 100644 index 0000000..181416b --- /dev/null +++ b/src/main/resources/application-flyway.yml @@ -0,0 +1,5 @@ +#flyway +spring.flyway: + enabled: true + # flyway 的 clean 命令会删除指定 schema 下的所有 table, 生产务必禁掉。 + clean-disabled: true \ No newline at end of file diff --git a/src/main/resources/application-logging.yml b/src/main/resources/application-logging.yml new file mode 100644 index 0000000..43cad31 --- /dev/null +++ b/src/main/resources/application-logging.yml @@ -0,0 +1,22 @@ +# 日志配置 +logging: + file: + # 日志文件名格式 + name: ${user.home}/logs/${spring.application.name}/${spring.application.name}.log + pattern: + # 日志记录格式[时间戳] [日志级别] [类名.方法名] [消息文本] + #file: xfile-%d{yyyy-MM-dd HH:mm:ss.SSS} %p %C.%M %m %n + file: xfile-%d{yyyy-MM-dd HH:mm:ss} %p %C.%M %m %n + logback: + rollingpolicy: + # 根据文件大小归档,超过10MB自动归档,次日启动服务时也会自动归档 + file-name-pattern: ${user.home}/logs/${spring.application.name}/${spring.application.name}.%d{yyyy-MM-dd}.%i.gz + # 最大日志文件大小(默认为10MB) + max-file-size: 1MB + # 要保留的日志备份的总大小(默认为0B) + total-size-cap: 1GB + # 保存存档日志文件的最大天数(默认为7天) + max-history: 30 + level: + root: info + org.elasticsearch: error diff --git a/src/main/resources/application-mybatis-plus.yml b/src/main/resources/application-mybatis-plus.yml new file mode 100644 index 0000000..415f45e --- /dev/null +++ b/src/main/resources/application-mybatis-plus.yml @@ -0,0 +1,9 @@ +mybatis-plus: + global-config: + banner: off + db-config: + #数据库id自增 + id-type: auto + configuration: + #MybatisEnumTypeHandler + default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler diff --git a/src/main/resources/application-release.yml b/src/main/resources/application-release.yml new file mode 100644 index 0000000..3cb9231 --- /dev/null +++ b/src/main/resources/application-release.yml @@ -0,0 +1,50 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/xnote?rewriteBatchedStatements=true + username: root + password: 123456 + redis: + host: localhost + port: 6379 + password: 123456 + mail: + host: smtp.qq.com + port: 587 + username: ${EMAIL_ADDRESS} + password: ${EMAIL_PASSWORD} + elasticsearch: + uris: http://localhost:9200 + username: xclhove + password: 123456 + +minio: + endpoint: http://localhost:9101 + remoteEndpoint: http://localhost:9101 + bucketName: xnote + accessKey: admin + secretKey: 12345678 + +xnote: + debug: + enable-mock-send-verification-code: true + enable-verify-verification-code: true + runner: + enable-check-redis-status: true + enable-check-minio-status: true + send-startup-email: true + import-data-from-database-to-elastic-search: false + update-image-size: false + create-default-note-type-for-user-of-none-type: false + replace-image-url-in-note: false + interceptor: + ip: + disable: true + max-frequency-per-minute: 120 + device: + disable: true + max-frequency-per-minute: 120 + image: + allow-size-of-byte: 10485760 + search: + height-light-content-max-length: 400 \ No newline at end of file diff --git a/src/main/resources/application-spring.yml b/src/main/resources/application-spring.yml new file mode 100644 index 0000000..d467346 --- /dev/null +++ b/src/main/resources/application-spring.yml @@ -0,0 +1,5 @@ +spring.application.name: xnote +spring.main.banner-mode: off +#为null时不返回属性 +spring.jackson.default-property-inclusion: NON_EMPTY +spring.main.allow-circular-references: true \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5e5fd3c..66e6d58 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,98 +1,8 @@ -#配置选择 dev:开发环境 | test:测试环境 | pro:生产环境 -spring.profiles.active: dev -#spring banner -spring.main.banner-mode: off -#mybatis-plus banner -mybatis-plus.global-config.banner: off -#数据库id自增 -mybatis-plus.global-config.db-config.id-type: auto -#数据库驱动名 -spring.datasource.driver-class-name: com.mysql.cj.jdbc.Driver -#服务名称 -spring.application.name: xnote -# 日志配置 -logging: - file: - # 日志文件名格式 - name: ${user.home}/logs/${spring.application.name}/${spring.application.name}.log - pattern: - # 日志记录格式[时间戳] [日志级别] [类名.方法名] [消息文本] - #file: xfile-%d{yyyy-MM-dd HH:mm:ss.SSS} %p %C.%M %m %n - file: xfile-%d{yyyy-MM-dd HH:mm:ss} %p %C.%M %m %n - logback: - rollingpolicy: - # 根据文件大小归档,超过10MB自动归档,次日启动服务时也会自动归档 - file-name-pattern: ${user.home}/logs/${spring.application.name}/${spring.application.name}.%d{yyyy-MM-dd}.%i.gz - # 最大日志文件大小(默认为10MB) - max-file-size: 1MB - # 要保留的日志备份的总大小(默认为0B) - total-size-cap: 1GB - # 保存存档日志文件的最大天数(默认为7天) - max-history: 30 -#flyway -spring.flyway: - enabled: true - # flyway 的 clean 命令会删除指定 schema 下的所有 table, 生产务必禁掉。 - clean-disabled: true -#swagger -spring.mvc.pathmatch.matching-strategy: ant_path_matcher -#MybatisEnumTypeHandler -mybatis-plus.configuration.default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler -spring.thymeleaf.check-template-location: false -#为null时不返回属性 -spring.jackson.default-property-inclusion: NON_EMPTY - -xnote: - ip: - max-frequency-per-minute: 120 #单个ip每分钟最大请求次数 - image: - allow-type: ^image/(png|jpg|jpeg|svg|ico|gif|bmp)+$ #允许上传的图片类型 - allow-size: 10485760 #10MB - page-request: - max-page-size: 100 #分页请求每页最大数量 - runner: - enable-keywords-convert-runner: false - enable-minio-status-check-runner: true - enable-redis-status-check-runner: true - enable-image-url-replace-runner: false - debug: - enable: true ---- -#应用服务WEB访问端口 -server.port: 8080 - -spring: - config: - activate: - on-profile: dev - #数据库配置 - datasource: - #MysqlUrl格式: jdbc:mysql://IP:Port/DataBaseName?parameter=value - url: jdbc:mysql://localhost:3306/xnote?rewriteBatchedStatements=true - #数据库用户名 - username: root - #数据库密码 - password: 123456 - redis: - host: localhost - port: 6379 - password: 123456 - database: 0 - mail: - host: smtp.qq.com - port: 587 - username: 2959169303@qq.com - password: 123456 - -#mybatis-plus 控制台打印完整带参数SQL语句 -mybatis-plus.configuration.log-impl: org.apache.ibatis.logging.stdout.StdOutImpl -logging: - level: - root: info - -#minio -minio: - endpoint: http://localhost:9090 #Minio服务所在地址 - bucketName: xnote #存储桶名称 - accessKey: admin #访问的key - secretKey: 12345678 #访问的秘钥 \ No newline at end of file +spring.profiles.include: + - spring + - mybatis-plus + - logging + - flyway + - xnote + - es + - @profileActive@ \ No newline at end of file diff --git a/src/main/resources/db/migration/V2.0__update_tables.sql b/src/main/resources/db/migration/V2.0__update_tables.sql new file mode 100644 index 0000000..29de647 --- /dev/null +++ b/src/main/resources/db/migration/V2.0__update_tables.sql @@ -0,0 +1,91 @@ +drop table if exists admin; +#------------------------------------------------------------- +create table if not exists note_type +( + id int auto_increment comment '类型ID' + primary key, + name varchar(255) not null comment '类型名称', + user_id int not null comment '用户ID', + constraint note_type_user_id_fk + foreign key (user_id) references user (id) + on update cascade on delete cascade +) + comment '笔记分类'; +#------------------------------------------------------------- +create table if not exists share_note_record +( + id int auto_increment + primary key, + code varchar(255) not null comment '分享码', + note_id int not null comment '笔记ID', + user_id int not null comment '用户ID', + expire_time datetime default current_timestamp not null comment '过期时间', + constraint share_note_pk unique (code), + constraint share_note_note_id_fk + foreign key (note_id) references note (id) + on update cascade on delete cascade, + constraint share_note_user_id_fk + foreign key (user_id) references user (id) + on update cascade on delete cascade +); +#------------------------------------------------------------- +alter table note + drop column access_code; +alter table note + drop column keywords; +alter table note + drop key note_title_user_id_uindex; + +alter table note + modify title varchar(255) default (now()) not null comment ' 笔记标题'; +alter table note + modify content text null comment '笔记内容'; +alter table note + add type_id int null comment '类型ID'; +alter table note + add constraint note_note_type_id_fk + foreign key (type_id) references note_type (id) + on update cascade on delete restrict; +#------------------------------------------------------------- +alter table user + add home_page_note_id int null comment '用户主页要显示的笔记的ID'; +alter table user + add constraint user_note_id_fk + foreign key (home_page_note_id) references note (id) + on update set null on delete set null; +alter table user + add image_storage_size + bigint default 104857600 null comment '图片存储空间大小(默认100MB)'; +#------------------------------------------------------------- +create table if not exists user_image +( + id int auto_increment primary key, + user_id int not null comment '用户ID', + image_id int not null comment '图片ID', + alias varchar(255) not null comment '图片别名', + constraint user_image_pk + unique (user_id, image_id), + constraint user_image_image_id_fk + foreign key (image_id) references image (id) + on update cascade on delete cascade, + constraint user_image_user_id_fk + foreign key (user_id) references user (id) + on update cascade on delete cascade +); + +# 迁移数据 +insert into user_image (user_id, image_id, alias) +select user_id, id, alias from image; +#------------------------------------------------------------- +alter table image drop column alias; +alter table image drop foreign key image_user_id_fk; +alter table image drop column user_id; + +alter table image + add size bigint not null comment '图片文件大小(bit)'; +alter table image + add owner_count int default 1 not null comment '拥有(上传)该图片的人数'; + +alter table image + add constraint image_pk unique (name); +#------------------------------------------------------------- \ No newline at end of file diff --git a/src/main/resources/mapper/ImageMapper.xml b/src/main/resources/mapper/ImageMapper.xml new file mode 100644 index 0000000..52a9cda --- /dev/null +++ b/src/main/resources/mapper/ImageMapper.xml @@ -0,0 +1,13 @@ + + + + + + update image + set owner_count = owner_count + #{incrementStep} + where id in + + #{id} + + + diff --git a/src/main/resources/mapper/NoteMapper.xml b/src/main/resources/mapper/NoteMapper.xml new file mode 100644 index 0000000..66a8405 --- /dev/null +++ b/src/main/resources/mapper/NoteMapper.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/src/main/resources/mapper/NoteTypeMapper.xml b/src/main/resources/mapper/NoteTypeMapper.xml new file mode 100644 index 0000000..2211e38 --- /dev/null +++ b/src/main/resources/mapper/NoteTypeMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/mapper/ShareNoteRecordMapper.xml b/src/main/resources/mapper/ShareNoteRecordMapper.xml new file mode 100644 index 0000000..f6d6538 --- /dev/null +++ b/src/main/resources/mapper/ShareNoteRecordMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/mapper/UserImageMapper.xml b/src/main/resources/mapper/UserImageMapper.xml new file mode 100644 index 0000000..fe2f9fa --- /dev/null +++ b/src/main/resources/mapper/UserImageMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/mapper/UserMapper.xml b/src/main/resources/mapper/UserMapper.xml new file mode 100644 index 0000000..31e5ce6 --- /dev/null +++ b/src/main/resources/mapper/UserMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index b305893..25b8b26 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -1,5 +1 @@ - - -

xnote

- - \ No newline at end of file +

XNote

\ No newline at end of file