From ae0f56a6dc9c1d3350eb533cdb93d905166d16cc Mon Sep 17 00:00:00 2001 From: Timi Date: Tue, 8 Jul 2025 16:31:30 +0800 Subject: [PATCH] Initial project --- .gitignore | 129 ++--- .idea/.gitignore | 3 + .idea/encodings.xml | 7 + .idea/misc.xml | 58 +++ .idea/uiDesigner.xml | 124 +++++ .idea/vcs.xml | 6 + LICENSE | 32 +- README.md | 8 +- pom.xml | 181 +++++++ .../java/com/imyeyu/server/TimiServerAPI.java | 75 +++ .../server/annotation/CaptchaValid.java | 22 + .../annotation/CaptchaValidInterceptor.java | 70 +++ .../server/annotation/EnableSetting.java | 25 + .../annotation/EnableSettingInterceptor.java | 40 ++ .../RequestRateLimitInterceptor.java | 49 ++ .../annotation/RequiredTokenInterceptor.java | 39 ++ .../com/imyeyu/server/bean/CaptchaFrom.java | 34 ++ .../java/com/imyeyu/server/bean/IOCBeans.java | 18 + .../server/bean/MultilingualHandler.java | 26 + .../com/imyeyu/server/bean/ResourceFile.java | 30 ++ .../com/imyeyu/server/config/AsyncConfig.java | 35 ++ .../com/imyeyu/server/config/BeanConfig.java | 16 + .../com/imyeyu/server/config/CORSConfig.java | 55 ++ .../com/imyeyu/server/config/MongoConfig.java | 24 + .../com/imyeyu/server/config/RedisConfig.java | 268 ++++++++++ .../imyeyu/server/config/SchedulerConfig.java | 33 ++ .../server/config/ThreadPoolConfig.java | 57 ++ .../com/imyeyu/server/config/WebConfig.java | 94 ++++ .../config/dbsource/ForeverMCDBConfig.java | 76 +++ .../server/config/dbsource/GiteaDBConfig.java | 104 ++++ .../config/dbsource/TimiServerDBConfig.java | 122 +++++ .../imyeyu/server/handler/GsonHandler.java | 57 ++ .../bill/controller/BillController.java | 49 ++ .../server/modules/bill/entity/Bill.java | 120 +++++ .../modules/bill/mapper/BillMapper.java | 13 + .../modules/bill/service/BillService.java | 13 + .../implement/BillServiceImplement.java | 27 + .../blog/controller/ArticleController.java | 93 ++++ .../blog/controller/BlogController.java | 32 ++ .../server/modules/blog/entity/Article.java | 95 ++++ .../modules/blog/entity/ArticleRanking.java | 33 ++ .../blog/entity/CommentRemindQueue.java | 36 ++ .../server/modules/blog/entity/Friend.java | 21 + .../modules/blog/mapper/ArticleMapper.java | 26 + .../modules/blog/mapper/FriendMapper.java | 20 + .../modules/blog/service/ArticleService.java | 48 ++ .../service/CommentRemindQueueService.java | 41 ++ .../modules/blog/service/FriendService.java | 16 + .../implement/ArticleServiceImplement.java | 113 ++++ .../CommentRemindQueueServiceImplement.java | 48 ++ .../implement/FriendServiceImplement.java | 27 + .../server/modules/blog/util/UserToken.java | 148 ++++++ .../modules/blog/vo/article/ArticleView.java | 24 + .../modules/blog/vo/article/ClassPage.java | 16 + .../modules/blog/vo/article/KeywordPage.java | 18 + .../modules/blog/vo/article/LabelPage.java | 16 + .../modules/common/bean/CommentSupport.java | 16 + .../modules/common/bean/EmailException.java | 30 ++ .../server/modules/common/bean/ImageType.java | 18 + .../modules/common/bean/SettingKey.java | 156 ++++++ .../common/controller/CommentController.java | 85 +++ .../common/controller/CommonController.java | 375 +++++++++++++ .../common/controller/IconController.java | 97 ++++ .../common/controller/UserController.java | 315 +++++++++++ .../modules/common/entity/Attachment.java | 74 +++ .../server/modules/common/entity/Comment.java | 68 +++ .../modules/common/entity/CommentReply.java | 45 ++ .../modules/common/entity/EmailQueue.java | 43 ++ .../modules/common/entity/EmailQueueLog.java | 23 + .../modules/common/entity/Feedback.java | 21 + .../server/modules/common/entity/Icon.java | 25 + .../modules/common/entity/Multilingual.java | 67 +++ .../server/modules/common/entity/Setting.java | 51 ++ .../server/modules/common/entity/Tag.java | 39 ++ .../server/modules/common/entity/Task.java | 40 ++ .../modules/common/entity/TaskDetail.java | 48 ++ .../modules/common/entity/Template.java | 33 ++ .../server/modules/common/entity/User.java | 68 +++ .../modules/common/entity/UserConfig.java | 33 ++ .../modules/common/entity/UserPrivacy.java | 51 ++ .../modules/common/entity/UserProfile.java | 66 +++ .../server/modules/common/entity/Version.java | 21 + .../common/mapper/AttachmentMapper.java | 31 ++ .../modules/common/mapper/CommentMapper.java | 40 ++ .../mapper/CommentRemindQueueMapper.java | 26 + .../common/mapper/CommentReplyMapper.java | 35 ++ .../common/mapper/EmailQueueLogMapper.java | 11 + .../common/mapper/EmailQueueMapper.java | 22 + .../modules/common/mapper/FeedbackMapper.java | 13 + .../modules/common/mapper/IconMapper.java | 43 ++ .../common/mapper/MultilingualMapper.java | 32 ++ .../modules/common/mapper/SettingMapper.java | 23 + .../modules/common/mapper/TagMapper.java | 11 + .../modules/common/mapper/TaskMapper.java | 16 + .../modules/common/mapper/TemplateMapper.java | 15 + .../common/mapper/UserConfigMapper.java | 13 + .../modules/common/mapper/UserMapper.java | 20 + .../common/mapper/UserPrivacyMapper.java | 13 + .../common/mapper/UserProfileMapper.java | 13 + .../modules/common/mapper/VersionMapper.java | 17 + .../common/service/AttachmentService.java | 56 ++ .../common/service/CommentReplyService.java | 21 + .../common/service/CommentService.java | 31 ++ .../common/service/EmailQueueService.java | 52 ++ .../common/service/FeedbackService.java | 14 + .../modules/common/service/IconService.java | 23 + .../common/service/MultilingualService.java | 24 + .../common/service/SettingService.java | 78 +++ .../modules/common/service/TagService.java | 17 + .../modules/common/service/TaskService.java | 23 + .../common/service/TemplateService.java | 12 + .../common/service/UserConfigService.java | 15 + .../common/service/UserPrivacyService.java | 15 + .../common/service/UserProfileService.java | 18 + .../modules/common/service/UserService.java | 149 ++++++ .../common/service/VersionService.java | 14 + .../implement/AttachmentServiceImplement.java | 149 ++++++ .../CommentReplyServiceImplement.java | 196 +++++++ .../implement/CommentServiceImplement.java | 159 ++++++ .../implement/EmailQueueServiceImplement.java | 50 ++ .../implement/FeedbackServiceImplement.java | 40 ++ .../implement/IconServiceImplement.java | 74 +++ .../MultilingualServiceImplement.java | 95 ++++ .../implement/SettingServiceImplement.java | 151 ++++++ .../implement/TagServiceImplement.java | 50 ++ .../implement/TaskServiceImplement.java | 28 + .../implement/TemplateServiceImplement.java | 31 ++ .../implement/UserConfigServiceImplement.java | 45 ++ .../UserPrivacyServiceImplement.java | 50 ++ .../UserProfileServiceImplement.java | 105 ++++ .../implement/UserServiceImplement.java | 396 ++++++++++++++ .../implement/VersionServiceImplement.java | 25 + .../server/modules/common/task/EmailTask.java | 222 ++++++++ .../task/MultilingualTranslateTask.java | 166 ++++++ .../modules/common/validation/UserName.java | 36 ++ .../common/validation/UserPassword.java | 34 ++ .../validtor/UserNameValidator.java | 35 ++ .../validtor/UserPasswordValidator.java | 32 ++ .../modules/common/vo/CaptchaRequest.java | 20 + .../modules/common/vo/FeedbackRequest.java | 22 + .../vo/attachment/AttachmentRequest.java | 21 + .../common/vo/attachment/AttachmentView.java | 15 + .../common/vo/comment/CommentReplyPage.java | 41 ++ .../common/vo/comment/CommentReplyView.java | 28 + .../common/vo/comment/CommentView.java | 34 ++ .../common/vo/comment/UserCommentPage.java | 18 + .../modules/common/vo/icon/AllResponse.java | 18 + .../modules/common/vo/icon/LabelPage.java | 18 + .../modules/common/vo/icon/NamePage.java | 18 + .../modules/common/vo/icon/UnicodePage.java | 18 + .../modules/common/vo/tag/TagRequest.java | 15 + .../vo/user/EmailVerifyCallbackRequest.java | 15 + .../modules/common/vo/user/LoginRequest.java | 25 + .../modules/common/vo/user/LoginResponse.java | 26 + .../common/vo/user/RegisterRequest.java | 32 ++ .../vo/user/UpdatePasswordByKeyRequest.java | 19 + .../common/vo/user/UpdatePasswordRequest.java | 18 + .../common/vo/user/UserProfileView.java | 21 + .../modules/common/vo/user/UserRequest.java | 33 ++ .../modules/common/vo/user/UserView.java | 56 ++ .../modules/forevermc/bean/ServerStatus.java | 158 ++++++ .../controller/ServerController.java | 115 ++++ .../modules/forevermc/entity/Server.java | 57 ++ .../forevermc/entity/ServerClient.java | 40 ++ .../forevermc/entity/ServerClientSrc.java | 52 ++ .../forevermc/mapper/ServerClientMapper.java | 17 + .../mapper/ServerClientSrcMapper.java | 17 + .../forevermc/mapper/ServerMapper.java | 11 + .../forevermc/service/ServerService.java | 35 ++ .../implement/ServerServiceImplement.java | 90 ++++ .../server/modules/git/bean/AttachType.java | 14 + .../server/modules/git/bean/GitCommit.java | 25 + .../server/modules/git/bean/gitea/API.java | 59 +++ .../server/modules/git/bean/gitea/Branch.java | 17 + .../server/modules/git/bean/gitea/File.java | 41 ++ .../modules/git/bean/gitea/GiteaResponse.java | 14 + .../modules/git/bean/gitea/Repository.java | 48 ++ .../modules/git/bean/hook/PostReceive.java | 21 + .../git/controller/DeveloperController.java | 43 ++ .../git/controller/IssueController.java | 108 ++++ .../git/controller/MergeController.java | 110 ++++ .../git/controller/ReleaseController.java | 41 ++ .../git/controller/RepositoryController.java | 166 ++++++ .../server/modules/git/entity/CommitLog.java | 33 ++ .../server/modules/git/entity/Developer.java | 37 ++ .../server/modules/git/entity/Issue.java | 120 +++++ .../server/modules/git/entity/Merge.java | 133 +++++ .../server/modules/git/entity/PushLog.java | 39 ++ .../server/modules/git/entity/Release.java | 33 ++ .../modules/git/mapper/DeveloperMapper.java | 15 + .../modules/git/mapper/IssueMapper.java | 35 ++ .../modules/git/mapper/MergeMapper.java | 23 + .../modules/git/mapper/ReleaseMapper.java | 23 + .../modules/git/service/DeveloperService.java | 17 + .../modules/git/service/IssueService.java | 20 + .../modules/git/service/MergeService.java | 20 + .../modules/git/service/ReleaseService.java | 14 + .../git/service/RepositoryService.java | 42 ++ .../implement/DeveloperServiceImplement.java | 66 +++ .../implement/IssueServiceImplement.java | 97 ++++ .../implement/MergeServiceImplement.java | 112 ++++ .../implement/ReleaseServiceImplement.java | 32 ++ .../implement/RepositoryServiceImplement.java | 169 ++++++ .../git/util/GiteaTimestampAdapter.java | 28 + .../git/vo/developer/DeveloperRequest.java | 15 + .../modules/git/vo/issue/CommentPage.java | 21 + .../modules/git/vo/issue/IssuePage.java | 21 + .../modules/git/vo/issue/IssueRequest.java | 37 ++ .../modules/git/vo/issue/IssueView.java | 22 + .../modules/git/vo/merge/MergePage.java | 21 + .../modules/git/vo/merge/MergeRequest.java | 44 ++ .../modules/git/vo/merge/MergeView.java | 25 + .../modules/git/vo/release/ReleasePage.java | 16 + .../modules/git/vo/release/ReleaseView.java | 19 + .../git/vo/repository/RepositoryView.java | 19 + .../modules/gitea/bean/ActionLogDTO.java | 63 +++ .../server/modules/gitea/entity/Action.java | 23 + .../modules/gitea/entity/Repository.java | 59 +++ .../server/modules/gitea/entity/User.java | 152 ++++++ .../modules/gitea/mapper/ActionMapper.java | 16 + .../modules/gitea/mapper/GiteaUserMapper.java | 11 + .../gitea/mapper/RepositoryMapper.java | 12 + .../modules/gitea/service/GiteaService.java | 25 + .../implement/GiteaServiceImplement.java | 65 +++ .../gitea/util/GiteaUTCTimestampAdapter.java | 32 ++ .../modules/gitea/vo/ActionLogView.java | 78 +++ .../lyric/controller/LyricController.java | 81 +++ .../server/modules/lyric/entity/Lyric.java | 28 + .../modules/lyric/entity/LyricCorrect.java | 56 ++ .../lyric/mapper/LyricCorrectMapper.java | 17 + .../modules/lyric/mapper/LyricMapper.java | 22 + .../lyric/service/LyricCorrectService.java | 22 + .../modules/lyric/service/LyricService.java | 27 + .../LyricCorrectServiceImplement.java | 108 ++++ .../implement/LyricServiceImplement.java | 140 +++++ .../modules/lyric/util/InitLyricSearch.java | 43 ++ .../modules/lyric/vo/LyricCorrectRequest.java | 37 ++ .../server/modules/lyric/vo/LyricRequest.java | 29 ++ .../annotation/RequiredFMCServerToken.java | 20 + .../RequiredFMCServerTokenInterceptor.java | 51 ++ .../modules/minecraft/bean/AttachType.java | 12 + .../controller/MinecraftController.java | 78 +++ .../minecraft/controller/PackController.java | 47 ++ .../controller/PlayerController.java | 127 +++++ .../minecraft/entity/MinecraftPack.java | 45 ++ .../minecraft/entity/MinecraftPackSource.java | 47 ++ .../minecraft/entity/MinecraftPlayer.java | 28 + .../modules/minecraft/mapper/PackMapper.java | 14 + .../minecraft/mapper/PlayerMapper.java | 20 + .../minecraft/service/FMCImageMapService.java | 38 ++ .../minecraft/service/PackService.java | 14 + .../minecraft/service/PlayerService.java | 38 ++ .../FMCImageMapServiceImplement.java | 62 +++ .../implement/PackServiceImplement.java | 25 + .../implement/PlayerServiceImplement.java | 151 ++++++ .../modules/minecraft/vo/LoginRequest.java | 15 + .../modules/minecraft/vo/TokenRequest.java | 18 + .../modules/minecraft/vo/TokenResponse.java | 26 + .../minecraft/vo/server/ReportRequest.java | 14 + .../server/modules/mirror/AbstractMirror.java | 158 ++++++ .../modules/mirror/AttachmentMirror.java | 88 ++++ .../modules/mirror/FabricAPIMirror.java | 152 ++++++ .../server/modules/mirror/MirrorSyncTask.java | 70 +++ .../modules/mirror/OpenJDKGithubMirror.java | 119 +++++ .../server/modules/mirror/OpenJDKMirror.java | 112 ++++ .../modules/mirror/OpenJDKTunaMirror.java | 83 +++ .../modules/mirror/bean/AttachType.java | 12 + .../mirror/controller/MirrorController.java | 64 +++ .../server/modules/mirror/data/FabricAPI.java | 26 + .../server/modules/mirror/data/OpenJDK.java | 44 ++ .../server/modules/mirror/entity/Mirror.java | 35 ++ .../modules/mirror/mapper/MirrorMapper.java | 29 ++ .../modules/mirror/service/MirrorService.java | 34 ++ .../implement/MirrorServiceImplement.java | 40 ++ .../server/modules/mirror/vo/MirrorView.java | 12 + .../modules/music/bean/ChannelBinding.java | 28 + .../modules/music/bean/pkg/BasePackage.java | 46 ++ .../music/bean/pkg/ControllerPackage.java | 47 ++ .../modules/music/bean/pkg/PlayerPackage.java | 21 + .../server/modules/music/core/Middleware.java | 100 ++++ .../handler/ControllerMessageHandler.java | 52 ++ .../music/handler/PlayerMessageHandler.java | 44 ++ .../runner/ControllerBootstrapRunner.java | 137 +++++ .../music/runner/PlayerBootstrapRunner.java | 94 ++++ .../modules/system/bean/FileSyncConfig.java | 50 ++ .../modules/system/bean/ServerFile.java | 250 +++++++++ .../modules/system/bean/ServerStatus.java | 246 +++++++++ .../modules/system/bean/TerminalPipe.java | 208 ++++++++ .../modules/system/bean/TransferFile.java | 19 + .../controller/AsyncTaskController.java | 70 +++ .../system/controller/FileController.java | 493 ++++++++++++++++++ .../system/controller/SystemController.java | 135 +++++ .../system/controller/TerminalController.java | 100 ++++ .../modules/system/entity/AsyncTask.java | 134 +++++ .../system/mapper/AsyncTaskMapper.java | 18 + .../system/service/AsyncTaskService.java | 30 ++ .../modules/system/service/FileService.java | 114 ++++ .../modules/system/service/SystemService.java | 18 + .../system/service/TerminalService.java | 63 +++ .../service/implement/AbstractAsyncTask.java | 214 ++++++++ .../implement/AsyncTaskServiceImplement.java | 159 ++++++ .../implement/FileServiceImplement.java | 271 ++++++++++ .../implement/SystemServiceImplement.java | 73 +++ .../implement/TerminalServiceImplement.java | 132 +++++ .../modules/system/task/ServerStatusTask.java | 235 +++++++++ .../modules/system/task/TerminalTask.java | 45 ++ .../system/task/async/DebugAsyncTask.java | 34 ++ .../task/async/FileCalcSizeAsyncTask.java | 54 ++ .../system/task/async/FileCopyAsyncTask.java | 73 +++ .../system/task/async/FileMoveAsyncTask.java | 85 +++ .../system/task/async/FileSyncTask.java | 150 ++++++ .../system/task/async/FileTarAsyncTask.java | 95 ++++ .../system/task/async/FileUnTarAsyncTask.java | 111 ++++ .../system/task/async/FileUnZipAsyncTask.java | 93 ++++ .../system/task/async/FileZipAsyncTask.java | 116 +++++ .../modules/system/util/LoggerFilter.java | 28 + .../modules/system/util/ResourceHandler.java | 47 ++ .../system/util/SystemAPIInterceptor.java | 51 ++ .../modules/system/vo/AsyncTaskView.java | 16 + .../modules/system/vo/ListFileToRequest.java | 21 + .../modules/system/vo/TempAttachRequest.java | 19 + .../system/vo/terminal/ExecCommand.java | 15 + src/main/java/com/imyeyu/server/util/AES.java | 84 +++ .../imyeyu/server/util/CaptchaManager.java | 189 +++++++ .../server/util/GsonSerializerAdapter.java | 59 +++ .../imyeyu/server/util/InitApplication.java | 125 +++++ .../com/imyeyu/server/util/RedisLanguage.java | 77 +++ .../imyeyu/server/util/RedisMultilingual.java | 27 + src/main/resources/application.yml | 101 ++++ src/main/resources/banner.txt | 9 + src/main/resources/logback.xml | 25 + .../resources/mapper/blog/ArticleMapper.xml | 65 +++ .../mapper/common/AttachmentMapper.xml | 22 + .../resources/mapper/common/CommentMapper.xml | 143 +++++ .../resources/mapper/common/IconMapper.xml | 35 ++ .../mapper/common/MultilingualMapper.xml | 11 + .../resources/mapper/common/SettingMapper.xml | 16 + .../resources/mapper/common/TaskMapper.xml | 55 ++ .../mapper/common/UserConfigMapper.xml | 5 + .../mapper/common/UserPrivacyMapper.xml | 4 + .../mapper/common/UserProfileMapper.xml | 5 + src/main/resources/mapper/git/IssueMapper.xml | 40 ++ src/main/resources/mapper/git/MergeMapper.xml | 40 ++ .../resources/mapper/git/ReleaseMapper.xml | 43 ++ .../resources/mapper/gitea/ActionMapper.xml | 52 ++ .../resources/mapper/minecraft/PackMapper.xml | 33 ++ .../mapper/minecraft/PlayerMapper.xml | 4 + .../mapper/system/AsyncTaskMapper.xml | 56 ++ src/main/resources/templates/EmailVerify.ftl | 58 +++ src/main/resources/templates/Footer.ftl | 11 + src/main/resources/templates/ReplyRemind.ftl | 92 ++++ .../resources/templates/ResetPassword.ftl | 63 +++ src/main/resources/templates/StyleSheet.ftl | 163 ++++++ src/test/java/test/SpringLang.java | 371 +++++++++++++ src/test/java/test/SpringTest.java | 113 ++++ src/test/java/test/Test.java | 27 + 356 files changed, 21123 insertions(+), 109 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/encodings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/uiDesigner.xml create mode 100644 .idea/vcs.xml create mode 100644 pom.xml create mode 100644 src/main/java/com/imyeyu/server/TimiServerAPI.java create mode 100644 src/main/java/com/imyeyu/server/annotation/CaptchaValid.java create mode 100644 src/main/java/com/imyeyu/server/annotation/CaptchaValidInterceptor.java create mode 100644 src/main/java/com/imyeyu/server/annotation/EnableSetting.java create mode 100644 src/main/java/com/imyeyu/server/annotation/EnableSettingInterceptor.java create mode 100644 src/main/java/com/imyeyu/server/annotation/RequestRateLimitInterceptor.java create mode 100644 src/main/java/com/imyeyu/server/annotation/RequiredTokenInterceptor.java create mode 100644 src/main/java/com/imyeyu/server/bean/CaptchaFrom.java create mode 100644 src/main/java/com/imyeyu/server/bean/IOCBeans.java create mode 100644 src/main/java/com/imyeyu/server/bean/MultilingualHandler.java create mode 100644 src/main/java/com/imyeyu/server/bean/ResourceFile.java create mode 100644 src/main/java/com/imyeyu/server/config/AsyncConfig.java create mode 100644 src/main/java/com/imyeyu/server/config/BeanConfig.java create mode 100644 src/main/java/com/imyeyu/server/config/CORSConfig.java create mode 100644 src/main/java/com/imyeyu/server/config/MongoConfig.java create mode 100644 src/main/java/com/imyeyu/server/config/RedisConfig.java create mode 100644 src/main/java/com/imyeyu/server/config/SchedulerConfig.java create mode 100644 src/main/java/com/imyeyu/server/config/ThreadPoolConfig.java create mode 100644 src/main/java/com/imyeyu/server/config/WebConfig.java create mode 100644 src/main/java/com/imyeyu/server/config/dbsource/ForeverMCDBConfig.java create mode 100644 src/main/java/com/imyeyu/server/config/dbsource/GiteaDBConfig.java create mode 100644 src/main/java/com/imyeyu/server/config/dbsource/TimiServerDBConfig.java create mode 100644 src/main/java/com/imyeyu/server/handler/GsonHandler.java create mode 100644 src/main/java/com/imyeyu/server/modules/bill/controller/BillController.java create mode 100644 src/main/java/com/imyeyu/server/modules/bill/entity/Bill.java create mode 100644 src/main/java/com/imyeyu/server/modules/bill/mapper/BillMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/bill/service/BillService.java create mode 100644 src/main/java/com/imyeyu/server/modules/bill/service/implement/BillServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/blog/controller/ArticleController.java create mode 100644 src/main/java/com/imyeyu/server/modules/blog/controller/BlogController.java create mode 100644 src/main/java/com/imyeyu/server/modules/blog/entity/Article.java create mode 100644 src/main/java/com/imyeyu/server/modules/blog/entity/ArticleRanking.java create mode 100644 src/main/java/com/imyeyu/server/modules/blog/entity/CommentRemindQueue.java create mode 100644 src/main/java/com/imyeyu/server/modules/blog/entity/Friend.java create mode 100644 src/main/java/com/imyeyu/server/modules/blog/mapper/ArticleMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/blog/mapper/FriendMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/blog/service/ArticleService.java create mode 100644 src/main/java/com/imyeyu/server/modules/blog/service/CommentRemindQueueService.java create mode 100644 src/main/java/com/imyeyu/server/modules/blog/service/FriendService.java create mode 100644 src/main/java/com/imyeyu/server/modules/blog/service/implement/ArticleServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/blog/service/implement/CommentRemindQueueServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/blog/service/implement/FriendServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/blog/util/UserToken.java create mode 100644 src/main/java/com/imyeyu/server/modules/blog/vo/article/ArticleView.java create mode 100644 src/main/java/com/imyeyu/server/modules/blog/vo/article/ClassPage.java create mode 100644 src/main/java/com/imyeyu/server/modules/blog/vo/article/KeywordPage.java create mode 100644 src/main/java/com/imyeyu/server/modules/blog/vo/article/LabelPage.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/bean/CommentSupport.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/bean/EmailException.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/bean/ImageType.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/bean/SettingKey.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/controller/CommentController.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/controller/CommonController.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/controller/IconController.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/controller/UserController.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/entity/Attachment.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/entity/Comment.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/entity/CommentReply.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/entity/EmailQueue.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/entity/EmailQueueLog.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/entity/Feedback.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/entity/Icon.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/entity/Multilingual.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/entity/Setting.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/entity/Tag.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/entity/Task.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/entity/TaskDetail.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/entity/Template.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/entity/User.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/entity/UserConfig.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/entity/UserPrivacy.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/entity/UserProfile.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/entity/Version.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/mapper/AttachmentMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/mapper/CommentMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/mapper/CommentRemindQueueMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/mapper/CommentReplyMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/mapper/EmailQueueLogMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/mapper/EmailQueueMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/mapper/FeedbackMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/mapper/IconMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/mapper/MultilingualMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/mapper/SettingMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/mapper/TagMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/mapper/TaskMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/mapper/TemplateMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/mapper/UserConfigMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/mapper/UserMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/mapper/UserPrivacyMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/mapper/UserProfileMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/mapper/VersionMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/AttachmentService.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/CommentReplyService.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/CommentService.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/EmailQueueService.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/FeedbackService.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/IconService.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/MultilingualService.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/SettingService.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/TagService.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/TaskService.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/TemplateService.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/UserConfigService.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/UserPrivacyService.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/UserProfileService.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/UserService.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/VersionService.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/implement/AttachmentServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/implement/CommentReplyServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/implement/CommentServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/implement/EmailQueueServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/implement/FeedbackServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/implement/IconServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/implement/MultilingualServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/implement/SettingServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/implement/TagServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/implement/TaskServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/implement/TemplateServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/implement/UserConfigServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/implement/UserPrivacyServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/implement/UserProfileServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/implement/UserServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/service/implement/VersionServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/task/EmailTask.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/task/MultilingualTranslateTask.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/validation/UserName.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/validation/UserPassword.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/validation/validtor/UserNameValidator.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/validation/validtor/UserPasswordValidator.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/CaptchaRequest.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/FeedbackRequest.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/attachment/AttachmentRequest.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/attachment/AttachmentView.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/comment/CommentReplyPage.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/comment/CommentReplyView.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/comment/CommentView.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/comment/UserCommentPage.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/icon/AllResponse.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/icon/LabelPage.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/icon/NamePage.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/icon/UnicodePage.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/tag/TagRequest.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/user/EmailVerifyCallbackRequest.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/user/LoginRequest.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/user/LoginResponse.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/user/RegisterRequest.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/user/UpdatePasswordByKeyRequest.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/user/UpdatePasswordRequest.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/user/UserProfileView.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/user/UserRequest.java create mode 100644 src/main/java/com/imyeyu/server/modules/common/vo/user/UserView.java create mode 100644 src/main/java/com/imyeyu/server/modules/forevermc/bean/ServerStatus.java create mode 100644 src/main/java/com/imyeyu/server/modules/forevermc/controller/ServerController.java create mode 100644 src/main/java/com/imyeyu/server/modules/forevermc/entity/Server.java create mode 100644 src/main/java/com/imyeyu/server/modules/forevermc/entity/ServerClient.java create mode 100644 src/main/java/com/imyeyu/server/modules/forevermc/entity/ServerClientSrc.java create mode 100644 src/main/java/com/imyeyu/server/modules/forevermc/mapper/ServerClientMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/forevermc/mapper/ServerClientSrcMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/forevermc/mapper/ServerMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/forevermc/service/ServerService.java create mode 100644 src/main/java/com/imyeyu/server/modules/forevermc/service/implement/ServerServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/bean/AttachType.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/bean/GitCommit.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/bean/gitea/API.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/bean/gitea/Branch.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/bean/gitea/File.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/bean/gitea/GiteaResponse.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/bean/gitea/Repository.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/bean/hook/PostReceive.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/controller/DeveloperController.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/controller/IssueController.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/controller/MergeController.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/controller/ReleaseController.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/controller/RepositoryController.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/entity/CommitLog.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/entity/Developer.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/entity/Issue.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/entity/Merge.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/entity/PushLog.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/entity/Release.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/mapper/DeveloperMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/mapper/IssueMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/mapper/MergeMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/mapper/ReleaseMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/service/DeveloperService.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/service/IssueService.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/service/MergeService.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/service/ReleaseService.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/service/RepositoryService.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/service/implement/DeveloperServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/service/implement/IssueServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/service/implement/MergeServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/service/implement/ReleaseServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/service/implement/RepositoryServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/util/GiteaTimestampAdapter.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/vo/developer/DeveloperRequest.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/vo/issue/CommentPage.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/vo/issue/IssuePage.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/vo/issue/IssueRequest.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/vo/issue/IssueView.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/vo/merge/MergePage.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/vo/merge/MergeRequest.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/vo/merge/MergeView.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/vo/release/ReleasePage.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/vo/release/ReleaseView.java create mode 100644 src/main/java/com/imyeyu/server/modules/git/vo/repository/RepositoryView.java create mode 100644 src/main/java/com/imyeyu/server/modules/gitea/bean/ActionLogDTO.java create mode 100644 src/main/java/com/imyeyu/server/modules/gitea/entity/Action.java create mode 100644 src/main/java/com/imyeyu/server/modules/gitea/entity/Repository.java create mode 100644 src/main/java/com/imyeyu/server/modules/gitea/entity/User.java create mode 100644 src/main/java/com/imyeyu/server/modules/gitea/mapper/ActionMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/gitea/mapper/GiteaUserMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/gitea/mapper/RepositoryMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/gitea/service/GiteaService.java create mode 100644 src/main/java/com/imyeyu/server/modules/gitea/service/implement/GiteaServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/gitea/util/GiteaUTCTimestampAdapter.java create mode 100644 src/main/java/com/imyeyu/server/modules/gitea/vo/ActionLogView.java create mode 100644 src/main/java/com/imyeyu/server/modules/lyric/controller/LyricController.java create mode 100644 src/main/java/com/imyeyu/server/modules/lyric/entity/Lyric.java create mode 100644 src/main/java/com/imyeyu/server/modules/lyric/entity/LyricCorrect.java create mode 100644 src/main/java/com/imyeyu/server/modules/lyric/mapper/LyricCorrectMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/lyric/mapper/LyricMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/lyric/service/LyricCorrectService.java create mode 100644 src/main/java/com/imyeyu/server/modules/lyric/service/LyricService.java create mode 100644 src/main/java/com/imyeyu/server/modules/lyric/service/implement/LyricCorrectServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/lyric/service/implement/LyricServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/lyric/util/InitLyricSearch.java create mode 100644 src/main/java/com/imyeyu/server/modules/lyric/vo/LyricCorrectRequest.java create mode 100644 src/main/java/com/imyeyu/server/modules/lyric/vo/LyricRequest.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/annotation/RequiredFMCServerToken.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/annotation/RequiredFMCServerTokenInterceptor.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/bean/AttachType.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/controller/MinecraftController.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/controller/PackController.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/controller/PlayerController.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/entity/MinecraftPack.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/entity/MinecraftPackSource.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/entity/MinecraftPlayer.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/mapper/PackMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/mapper/PlayerMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/service/FMCImageMapService.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/service/PackService.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/service/PlayerService.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/service/implement/FMCImageMapServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/service/implement/PackServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/service/implement/PlayerServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/vo/LoginRequest.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/vo/TokenRequest.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/vo/TokenResponse.java create mode 100644 src/main/java/com/imyeyu/server/modules/minecraft/vo/server/ReportRequest.java create mode 100644 src/main/java/com/imyeyu/server/modules/mirror/AbstractMirror.java create mode 100644 src/main/java/com/imyeyu/server/modules/mirror/AttachmentMirror.java create mode 100644 src/main/java/com/imyeyu/server/modules/mirror/FabricAPIMirror.java create mode 100644 src/main/java/com/imyeyu/server/modules/mirror/MirrorSyncTask.java create mode 100644 src/main/java/com/imyeyu/server/modules/mirror/OpenJDKGithubMirror.java create mode 100644 src/main/java/com/imyeyu/server/modules/mirror/OpenJDKMirror.java create mode 100644 src/main/java/com/imyeyu/server/modules/mirror/OpenJDKTunaMirror.java create mode 100644 src/main/java/com/imyeyu/server/modules/mirror/bean/AttachType.java create mode 100644 src/main/java/com/imyeyu/server/modules/mirror/controller/MirrorController.java create mode 100644 src/main/java/com/imyeyu/server/modules/mirror/data/FabricAPI.java create mode 100644 src/main/java/com/imyeyu/server/modules/mirror/data/OpenJDK.java create mode 100644 src/main/java/com/imyeyu/server/modules/mirror/entity/Mirror.java create mode 100644 src/main/java/com/imyeyu/server/modules/mirror/mapper/MirrorMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/mirror/service/MirrorService.java create mode 100644 src/main/java/com/imyeyu/server/modules/mirror/service/implement/MirrorServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/mirror/vo/MirrorView.java create mode 100644 src/main/java/com/imyeyu/server/modules/music/bean/ChannelBinding.java create mode 100644 src/main/java/com/imyeyu/server/modules/music/bean/pkg/BasePackage.java create mode 100644 src/main/java/com/imyeyu/server/modules/music/bean/pkg/ControllerPackage.java create mode 100644 src/main/java/com/imyeyu/server/modules/music/bean/pkg/PlayerPackage.java create mode 100644 src/main/java/com/imyeyu/server/modules/music/core/Middleware.java create mode 100644 src/main/java/com/imyeyu/server/modules/music/handler/ControllerMessageHandler.java create mode 100644 src/main/java/com/imyeyu/server/modules/music/handler/PlayerMessageHandler.java create mode 100644 src/main/java/com/imyeyu/server/modules/music/runner/ControllerBootstrapRunner.java create mode 100644 src/main/java/com/imyeyu/server/modules/music/runner/PlayerBootstrapRunner.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/bean/FileSyncConfig.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/bean/ServerFile.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/bean/ServerStatus.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/bean/TerminalPipe.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/bean/TransferFile.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/controller/AsyncTaskController.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/controller/FileController.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/controller/SystemController.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/controller/TerminalController.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/entity/AsyncTask.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/mapper/AsyncTaskMapper.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/service/AsyncTaskService.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/service/FileService.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/service/SystemService.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/service/TerminalService.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/service/implement/AbstractAsyncTask.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/service/implement/AsyncTaskServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/service/implement/FileServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/service/implement/SystemServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/service/implement/TerminalServiceImplement.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/task/ServerStatusTask.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/task/TerminalTask.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/task/async/DebugAsyncTask.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/task/async/FileCalcSizeAsyncTask.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/task/async/FileCopyAsyncTask.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/task/async/FileMoveAsyncTask.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/task/async/FileSyncTask.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/task/async/FileTarAsyncTask.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/task/async/FileUnTarAsyncTask.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/task/async/FileUnZipAsyncTask.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/task/async/FileZipAsyncTask.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/util/LoggerFilter.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/util/ResourceHandler.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/util/SystemAPIInterceptor.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/vo/AsyncTaskView.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/vo/ListFileToRequest.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/vo/TempAttachRequest.java create mode 100644 src/main/java/com/imyeyu/server/modules/system/vo/terminal/ExecCommand.java create mode 100644 src/main/java/com/imyeyu/server/util/AES.java create mode 100644 src/main/java/com/imyeyu/server/util/CaptchaManager.java create mode 100644 src/main/java/com/imyeyu/server/util/GsonSerializerAdapter.java create mode 100644 src/main/java/com/imyeyu/server/util/InitApplication.java create mode 100644 src/main/java/com/imyeyu/server/util/RedisLanguage.java create mode 100644 src/main/java/com/imyeyu/server/util/RedisMultilingual.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/banner.txt create mode 100644 src/main/resources/logback.xml create mode 100644 src/main/resources/mapper/blog/ArticleMapper.xml create mode 100644 src/main/resources/mapper/common/AttachmentMapper.xml create mode 100644 src/main/resources/mapper/common/CommentMapper.xml create mode 100644 src/main/resources/mapper/common/IconMapper.xml create mode 100644 src/main/resources/mapper/common/MultilingualMapper.xml create mode 100644 src/main/resources/mapper/common/SettingMapper.xml create mode 100644 src/main/resources/mapper/common/TaskMapper.xml create mode 100644 src/main/resources/mapper/common/UserConfigMapper.xml create mode 100644 src/main/resources/mapper/common/UserPrivacyMapper.xml create mode 100644 src/main/resources/mapper/common/UserProfileMapper.xml create mode 100644 src/main/resources/mapper/git/IssueMapper.xml create mode 100644 src/main/resources/mapper/git/MergeMapper.xml create mode 100644 src/main/resources/mapper/git/ReleaseMapper.xml create mode 100644 src/main/resources/mapper/gitea/ActionMapper.xml create mode 100644 src/main/resources/mapper/minecraft/PackMapper.xml create mode 100644 src/main/resources/mapper/minecraft/PlayerMapper.xml create mode 100644 src/main/resources/mapper/system/AsyncTaskMapper.xml create mode 100644 src/main/resources/templates/EmailVerify.ftl create mode 100644 src/main/resources/templates/Footer.ftl create mode 100644 src/main/resources/templates/ReplyRemind.ftl create mode 100644 src/main/resources/templates/ResetPassword.ftl create mode 100644 src/main/resources/templates/StyleSheet.ftl create mode 100644 src/test/java/test/SpringLang.java create mode 100644 src/test/java/test/SpringTest.java create mode 100644 src/test/java/test/Test.java diff --git a/.gitignore b/.gitignore index c6d98d1..a4ac6eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,98 +1,43 @@ -# ---> JetBrains -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 +/config +/data +/logs +/target -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf +multilingualField/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/multilingualField/ +!**/src/test/**/multilingualField/ -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ *.iws +*.iml +*.ipr -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# SonarLint plugin -.idea/sonarlint/ - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -# ---> Maven -target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties -# https://github.com/takari/maven-wrapper#usage-without-binary-jar -.mvn/wrapper/maven-wrapper.jar - -# Eclipse m2e generated files -# Eclipse Core -.project -# JDT-specific (Eclipse Java Development Tools) +### Eclipse ### +.apt_generated .classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..c79ede3 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..454bc98 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE index 9639e2b..f58eeca 100644 --- a/LICENSE +++ b/LICENSE @@ -1,18 +1,20 @@ -MIT License +The MIT License (MIT) +Copyright © 2021 imyeyu.com -Copyright (c) 2025 timi +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 57114fd..039fb67 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ -# TimiServerAPI +## Timi 服务器数据中心 -Timi 总后端 \ No newline at end of file +[我的博客](https://www.imyeyu.net)、[timi-icon](https://icon.imyeyu.net) 等我开发的软件可能会使用此数据接口 + +技术栈:SpringBoot MariaDB MyBatis Redis + +[timijava](https://github.com/imyeyu/timijava) \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..9bdcc88 --- /dev/null +++ b/pom.xml @@ -0,0 +1,181 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.0 + + + + com.imyeyu.timiserverapi + TimiServerAPI + 1.0.0 + jar + TimiServerAPI + imyeyu.com API + + + 3.4.0 + 21 + 21 + UTF-8 + + + + + apache-maven + https://repo.maven.apache.org/maven2/ + + + + + compile + + + org.springframework.boot + spring-boot-maven-plugin + ${springboot.version} + + + + repackage + + + + + true + com.imyeyu.server.TimiServerAPI + ${project.artifactId} + + + + + + + + com.imyeyu.spring + timi-spring + 0.0.1 + + + com.imyeyu.network + timi-network + 0.0.1 + + + com.imyeyu.lang + timi-lang + 0.0.1 + + + org.springframework.boot + spring-boot-starter-test + ${springboot.version} + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.mariadb.jdbc + mariadb-java-client + 3.5.3 + + + org.springframework.boot + spring-boot-starter-freemarker + ${springboot.version} + + + org.springframework.boot + spring-boot-starter-data-mongodb + ${springboot.version} + + + junit + junit + test + + + io.netty + netty-codec-http + + + org.apache.commons + commons-exec + 1.3 + + + org.projectlombok + lombok + 1.18.34 + provided + + + org.eclipse.jgit + org.eclipse.jgit + 6.7.0.202309050840-r + + + org.eclipse.jgit + org.eclipse.jgit.archive + 6.7.0.202309050840-r + + + org.apache.commons + commons-compress + 1.26.1 + + + org.dom4j + dom4j + 2.1.4 + + + xml-apis + xml-apis + + + + + com.github.oshi + oshi-core + 6.8.0 + + + net.coobird + thumbnailator + 0.4.20 + + + org.apache.tika + tika-core + 2.9.2 + + + org.jcodec + jcodec + 0.2.5 + + + org.jcodec + jcodec-javase + 0.2.5 + + + org.jsoup + jsoup + 1.18.1 + + + commons-codec + commons-codec + 1.17.0 + + + diff --git a/src/main/java/com/imyeyu/server/TimiServerAPI.java b/src/main/java/com/imyeyu/server/TimiServerAPI.java new file mode 100644 index 0000000..3dbf101 --- /dev/null +++ b/src/main/java/com/imyeyu/server/TimiServerAPI.java @@ -0,0 +1,75 @@ +package com.imyeyu.server; + +import com.imyeyu.io.IO; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.Language; +import com.imyeyu.java.ref.Ref; +import com.imyeyu.spring.TimiSpring; +import com.imyeyu.utils.OS; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.env.Environment; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import java.io.File; + +/** + * 夜雨综合数据中心接口 + * + *

本端所有接口面向用户,不做管理接口,数据管理将使用 JavaFX + * + * @author 夜雨 + * @since 2021-02-23 21:35 + */ +@Slf4j +@SpringBootApplication(scanBasePackages = {"com.imyeyu.server", "com.imyeyu.spring"}) +@EnableTransactionManagement +public class TimiServerAPI implements OS.FileSystem, ApplicationContextAware { + + private static final String DEV_LANG_CONFIG = "dev.lang"; + + public static ApplicationContext applicationContext; + + @Override + public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException { + TimiServerAPI.applicationContext = applicationContext; + } + + public static Language getUserLanguage() { + Language userLanguage = TimiSpring.getLanguage(); + Environment env = applicationContext.getBean(Environment.class); + if (env.containsProperty(DEV_LANG_CONFIG)) { + String property = env.getProperty(DEV_LANG_CONFIG); + if (TimiJava.isNotEmpty(property)) { + userLanguage = Ref.toType(Language.class, property); + } + } + return userLanguage; + } + + public static void main(String[] args) { + try { + { + // 导出配置 + String[] files = {"application.yml", "logback.xml"}; + for (int i = 0; i < files.length; i++) { + File file = new File("config" + SEP + files[i]); + if (!file.exists() || !file.isFile()) { + log.info("exporting default config at {}", file.getAbsolutePath()); + IO.resourceToDisk(TimiServerAPI.class, files[i], file.getAbsolutePath()); + } + } + } + + // 启动 SpringBoot + SpringApplication.run(TimiServerAPI.class, args); + } catch (Exception e) { + log.error("launch error", e); + } + } +} diff --git a/src/main/java/com/imyeyu/server/annotation/CaptchaValid.java b/src/main/java/com/imyeyu/server/annotation/CaptchaValid.java new file mode 100644 index 0000000..033b477 --- /dev/null +++ b/src/main/java/com/imyeyu/server/annotation/CaptchaValid.java @@ -0,0 +1,22 @@ +package com.imyeyu.server.annotation; + +import com.imyeyu.server.bean.CaptchaFrom; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 图形验证码校验注解 + * + * @author 夜雨 + * @since 2023-07-15 10:09 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface CaptchaValid { + + /** @return 验证码来源 */ + CaptchaFrom value(); +} diff --git a/src/main/java/com/imyeyu/server/annotation/CaptchaValidInterceptor.java b/src/main/java/com/imyeyu/server/annotation/CaptchaValidInterceptor.java new file mode 100644 index 0000000..07ccf0b --- /dev/null +++ b/src/main/java/com/imyeyu/server/annotation/CaptchaValidInterceptor.java @@ -0,0 +1,70 @@ +package com.imyeyu.server.annotation; + +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.server.bean.CaptchaFrom; +import com.imyeyu.server.util.CaptchaManager; +import com.imyeyu.spring.bean.CaptchaData; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; + +/** + * 图形验证码校验注解处理器 + * + * @author 夜雨 + * @since 2023-07-15 10:01 + */ +@Slf4j +@Aspect +@Component +public class CaptchaValidInterceptor { + + @Value("${spring.profiles.active}") + private String env; + + @Autowired + private CaptchaManager captchaManager; + + /** 注入注解 */ + @Pointcut("@annotation(com.imyeyu.server.annotation.CaptchaValid)") + public void captchaPointCut() { + } + + /** + * 执行前 + * + * @param joinPoint 切入点 + */ + @Before("captchaPointCut()") + public void doBefore(JoinPoint joinPoint) { + try { + if (env.startsWith("dev")) { + // 开发环境不校验 + return; + } + if (joinPoint.getSignature() instanceof MethodSignature ms) { + Method method = joinPoint.getTarget().getClass().getMethod(ms.getName(), ms.getParameterTypes()); + CaptchaValid annotation = method.getAnnotation(CaptchaValid.class); + CaptchaFrom from = annotation.value(); + + Object[] args = joinPoint.getArgs(); + for (int i = 0; i < args.length; i++) { + if (args[i] instanceof CaptchaData captchaData) { + // 校验请求参数的验证码 + captchaManager.test(captchaData.getCaptcha(), from.toString()); + break; + } + } + } + } catch (NoSuchMethodException e) { + throw new RuntimeException("TODO CaptchaValidInterceptor error"); + } + } +} diff --git a/src/main/java/com/imyeyu/server/annotation/EnableSetting.java b/src/main/java/com/imyeyu/server/annotation/EnableSetting.java new file mode 100644 index 0000000..f78ae6f --- /dev/null +++ b/src/main/java/com/imyeyu/server/annotation/EnableSetting.java @@ -0,0 +1,25 @@ +package com.imyeyu.server.annotation; + +import com.imyeyu.server.modules.common.bean.SettingKey; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 启用配置注解 + * + * @author 夜雨 + * @since 2023-07-15 10:00 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface EnableSetting { + + /** @return 配置键 */ + SettingKey value(); + + /** @return 未启用配置时响应消息语言映射键 */ + String message() default "service.offline"; +} diff --git a/src/main/java/com/imyeyu/server/annotation/EnableSettingInterceptor.java b/src/main/java/com/imyeyu/server/annotation/EnableSettingInterceptor.java new file mode 100644 index 0000000..1ee0698 --- /dev/null +++ b/src/main/java/com/imyeyu/server/annotation/EnableSettingInterceptor.java @@ -0,0 +1,40 @@ +package com.imyeyu.server.annotation; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.common.service.SettingService; +import org.springframework.context.annotation.Lazy; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +/** + * 启用配置注解处理器 + * + * @author 夜雨 + * @since 2023-07-15 10:01 + */ +@Component +@RequiredArgsConstructor(onConstructor_ = {@Lazy}) +public class EnableSettingInterceptor implements HandlerInterceptor { + + private final SettingService service; + + public boolean preHandle(@NonNull HttpServletRequest req, @NonNull HttpServletResponse resp, @NonNull Object handler) { + if (handler instanceof HandlerMethod handlerMethod) { + EnableSetting annotation = handlerMethod.getMethodAnnotation(EnableSetting.class); + if (annotation == null) { + return true; + } + if (service.is(annotation.value())) { + return true; + } + throw new TimiException(TimiCode.ERROR_SERVICE_OFF, annotation.message()); + } + return true; + } +} diff --git a/src/main/java/com/imyeyu/server/annotation/RequestRateLimitInterceptor.java b/src/main/java/com/imyeyu/server/annotation/RequestRateLimitInterceptor.java new file mode 100644 index 0000000..5eab945 --- /dev/null +++ b/src/main/java/com/imyeyu/server/annotation/RequestRateLimitInterceptor.java @@ -0,0 +1,49 @@ +package com.imyeyu.server.annotation; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.TimiSpring; +import com.imyeyu.spring.annotation.RequestRateLimitAbstractInterceptor; +import com.imyeyu.spring.util.Redis; +import com.imyeyu.utils.Time; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +/** + * 请求频率限制处理器 + * + * @author 夜雨 + * @since 2021-08-16 18:07 + */ +@Slf4j +@Component +public class RequestRateLimitInterceptor extends RequestRateLimitAbstractInterceptor { + + @Autowired + @Qualifier("redisRateLimit") + private Redis redisRequestRateLimit; + + @Override + public boolean beforeRun(HttpServletRequest req, HttpServletResponse resp, String id, int cycle, int limit) { + // 键 + String key = "TimiServerAPI." + TimiSpring.getRequestIP() + "." + id; + if (redisRequestRateLimit.has(key)) { + Integer count = redisRequestRateLimit.get(key); + if (count != null) { + if (count < limit) { + redisRequestRateLimit.setAndKeepTTL(key, ++count); + } else { + log.warn("请求频率过高:[" + key + "].C" + count + "L" + limit); + throw new TimiException(TimiCode.REQUEST_BAD).msgKey("request_rate_limit"); + } + } + return true; + } + redisRequestRateLimit.set(key, 0, Time.S * cycle); + return true; + } +} diff --git a/src/main/java/com/imyeyu/server/annotation/RequiredTokenInterceptor.java b/src/main/java/com/imyeyu/server/annotation/RequiredTokenInterceptor.java new file mode 100644 index 0000000..7aec62e --- /dev/null +++ b/src/main/java/com/imyeyu/server/annotation/RequiredTokenInterceptor.java @@ -0,0 +1,39 @@ +package com.imyeyu.server.annotation; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.blog.util.UserToken; +import com.imyeyu.spring.TimiSpring; +import com.imyeyu.spring.annotation.RequiredToken; +import com.imyeyu.spring.annotation.RequiredTokenAbstractInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 令牌验证注解处理器 + * + * @author 夜雨 + * @since 2021-08-16 18:07 + */ +@Slf4j +@Component +public class RequiredTokenInterceptor extends RequiredTokenAbstractInterceptor { + + @Autowired + private UserToken userToken; + + public RequiredTokenInterceptor() { + super(RequiredToken.class); + } + + @Override + protected boolean beforeRun(HttpServletRequest req, HttpServletResponse resp) { + if (userToken.isInvalid(TimiSpring.getToken())) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("token.illegal"); + } + return true; + } +} diff --git a/src/main/java/com/imyeyu/server/bean/CaptchaFrom.java b/src/main/java/com/imyeyu/server/bean/CaptchaFrom.java new file mode 100644 index 0000000..9804aef --- /dev/null +++ b/src/main/java/com/imyeyu/server/bean/CaptchaFrom.java @@ -0,0 +1,34 @@ +package com.imyeyu.server.bean; + +/** + * 验证码来源 + * + * @author 夜雨 + * @since 2023-07-16 10:12 + */ +public enum CaptchaFrom { + + /** 注册 */ + REGISTER, + + /** 登录 */ + LOGIN, + + /** 忘记密码 */ + RESET_PASSWORD, + + /** 评论 */ + COMMENT, + + /** 评论回复 */ + COMMENT_REPLY, + + /** Git 反馈 */ + GIT_ISSUE, + + /** Git 合并请求 */ + GIT_MERGE, + + /** 歌词修正申请 */ + LYRIC_CORRECT +} diff --git a/src/main/java/com/imyeyu/server/bean/IOCBeans.java b/src/main/java/com/imyeyu/server/bean/IOCBeans.java new file mode 100644 index 0000000..6c04a71 --- /dev/null +++ b/src/main/java/com/imyeyu/server/bean/IOCBeans.java @@ -0,0 +1,18 @@ +package com.imyeyu.server.bean; + +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; +import org.yaml.snakeyaml.Yaml; + +/** + * @author 夜雨 + * @since 2025-01-13 11:42 + */ +@Component +public class IOCBeans { + + @Bean + public Yaml yaml() { + return new Yaml(); + } +} diff --git a/src/main/java/com/imyeyu/server/bean/MultilingualHandler.java b/src/main/java/com/imyeyu/server/bean/MultilingualHandler.java new file mode 100644 index 0000000..693f19d --- /dev/null +++ b/src/main/java/com/imyeyu/server/bean/MultilingualHandler.java @@ -0,0 +1,26 @@ +package com.imyeyu.server.bean; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author 夜雨 + * @since 2023-10-25 10:10 + */ +public interface MultilingualHandler { + + /** + * + * + * @author 夜雨 + * @since 2023-10-25 10:25 + */ + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @interface MultilingualField { + + String[] args() default {}; + } +} diff --git a/src/main/java/com/imyeyu/server/bean/ResourceFile.java b/src/main/java/com/imyeyu/server/bean/ResourceFile.java new file mode 100644 index 0000000..4cd59f0 --- /dev/null +++ b/src/main/java/com/imyeyu/server/bean/ResourceFile.java @@ -0,0 +1,30 @@ +package com.imyeyu.server.bean; + +import lombok.Data; +import com.imyeyu.utils.OS; + +import java.io.InputStream; + +/** + * 资源文件 + * + * @author 夜雨 + * @since 2021-07-31 15:31 + */ +@Data +public class ResourceFile implements OS.FileSystem { + + /** 服务器文件所在路径 */ + private String path; + + /** 文件名 */ + private String name; + + /** 文件数据流 */ + private InputStream inputStream; + + /** @return 完整绝对路径 */ + public String getFullPath() { + return path + SEP + name; + } +} diff --git a/src/main/java/com/imyeyu/server/config/AsyncConfig.java b/src/main/java/com/imyeyu/server/config/AsyncConfig.java new file mode 100644 index 0000000..5797fec --- /dev/null +++ b/src/main/java/com/imyeyu/server/config/AsyncConfig.java @@ -0,0 +1,35 @@ +package com.imyeyu.server.config; + +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; + +import java.util.Arrays; + +/** + * 异步线程池配置 + * + * @author 夜雨 + * @since 2023-08-21 16:22 + */ +@Slf4j +@EnableAsync +@Configuration +public class AsyncConfig implements AsyncConfigurer { + + @Override + public @NotNull AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return (e, method, obj) -> { + log.info("Exception message - {}", e.getMessage()); + log.info("Method name - {}", method.getName()); + log.info("Parameter values - {}", Arrays.toString(obj)); + if (e instanceof Exception exception) { + log.info("exception: {}", exception.getMessage()); + } + log.error("async uncaught error", e); + }; + } +} diff --git a/src/main/java/com/imyeyu/server/config/BeanConfig.java b/src/main/java/com/imyeyu/server/config/BeanConfig.java new file mode 100644 index 0000000..2b1d2b0 --- /dev/null +++ b/src/main/java/com/imyeyu/server/config/BeanConfig.java @@ -0,0 +1,16 @@ +package com.imyeyu.server.config; + +import com.google.gson.Gson; +import org.springframework.context.annotation.Configuration; + +/** + * @author 夜雨 + * @since 2025-05-16 18:53 + */ +@Configuration +public class BeanConfig { + + public Gson gson() { + return new Gson(); + } +} diff --git a/src/main/java/com/imyeyu/server/config/CORSConfig.java b/src/main/java/com/imyeyu/server/config/CORSConfig.java new file mode 100644 index 0000000..57aedae --- /dev/null +++ b/src/main/java/com/imyeyu/server/config/CORSConfig.java @@ -0,0 +1,55 @@ +package com.imyeyu.server.config; + +import jakarta.servlet.Filter; +import lombok.Data; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Arrays; + +/** + * 跨域控制 + * + * @author 夜雨 + * @since 2021-05-14 09:21 + */ +@Data +@Configuration +@EnableAutoConfiguration +@ConfigurationProperties(prefix = "cors") +public class CORSConfig { + + /** 允许跨域的地址 */ + private String[] allowOrigin; + + /** 是否允许请求带有验证信息 */ + private boolean allowCredentials; + + /** 允许请求的方法 */ + private String allowMethods; + + /** 允许服务端访问的客户端请求头 */ + private String allowHeaders; + + @Bean + public FilterRegistrationBean corsFilter() { + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedHeader(allowHeaders); + config.addAllowedMethod(allowMethods); + config.setAllowCredentials(allowCredentials); + config.setAllowedOriginPatterns(Arrays.asList(allowOrigin)); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + FilterRegistrationBean bean = new FilterRegistrationBean<>(new CorsFilter(source)); + bean.setOrder(Ordered.HIGHEST_PRECEDENCE); + return bean; + } +} diff --git a/src/main/java/com/imyeyu/server/config/MongoConfig.java b/src/main/java/com/imyeyu/server/config/MongoConfig.java new file mode 100644 index 0000000..a5c02f5 --- /dev/null +++ b/src/main/java/com/imyeyu/server/config/MongoConfig.java @@ -0,0 +1,24 @@ +package com.imyeyu.server.config; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.gridfs.GridFSBucket; +import com.mongodb.client.gridfs.GridFSBuckets; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author 夜雨 + * @since 2024-02-23 10:55 + */ +@Configuration +public class MongoConfig { + + @Value("${spring.data.mongodb.database}") + private String db; + + @Bean + public GridFSBucket gridFSBucket(MongoClient mongoClient) { + return GridFSBuckets.create(mongoClient.getDatabase(db)); + } +} diff --git a/src/main/java/com/imyeyu/server/config/RedisConfig.java b/src/main/java/com/imyeyu/server/config/RedisConfig.java new file mode 100644 index 0000000..54c1877 --- /dev/null +++ b/src/main/java/com/imyeyu/server/config/RedisConfig.java @@ -0,0 +1,268 @@ +package com.imyeyu.server.config; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.server.modules.blog.entity.ArticleRanking; +import com.imyeyu.server.modules.common.entity.Multilingual; +import com.imyeyu.spring.bean.RedisConfigParams; +import com.imyeyu.spring.config.AbstractRedisConfig; +import com.imyeyu.spring.util.Redis; +import com.imyeyu.spring.util.RedisSerializers; +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.serializer.support.DeserializingConverter; +import org.springframework.core.serializer.support.SerializingConverter; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; +import org.springframework.lang.Nullable; + +import java.time.Duration; + +/** + * Redis 配置 + * + * @author 夜雨 + * @since 2021-02-23 21:36 + */ +@Data +@Configuration +@EqualsAndHashCode(callSuper = true) +@EnableAutoConfiguration +@ConfigurationProperties(prefix = "spring.redis") +public class RedisConfig extends AbstractRedisConfig { + + // ---------- 连接配置 ---------- + + /** 地址 */ + private String host; + + /** 端口 */ + private int port; + + /** 密码 */ + private String password; + + /** 超时(毫秒) */ + private int timeout; + + /** 连接池 */ + private Lettuce lettuce; + + /** 数据库 */ + private Database database; + + /** + * 连接池 + * + * @author 夜雨 + * @since 2023-08-21 16:23 + */ + @Data + public static class Lettuce { + + /** 配置 */ + private Pool pool; + + /** + * 配置 + * + * @author 夜雨 + * @since 2023-08-21 16:23 + */ + @Data + public static class Pool { + + /** 最大活跃连接 */ + private int maxActive; + + /** 最小空闲连接 */ + private int minIdle; + + /** 最大空闲连接 */ + private int maxIdle; + + /** 最大等待时间(秒) */ + private int maxWait; + } + } + + /** + * 数据库 + * + * @author 夜雨 + * @since 2023-08-21 16:25 + */ + @Data + public static class Database { + + /** 分布式锁 */ + private int locker; + + /** 多语言环境 */ + private int language; + + /** 多语言键环境 */ + private int languageMap; + + /** 文章排位 */ + private int articleRanking; + + /** 文章阅读记录 */ + private int articleRead; + + /** 用户登录令牌 */ + private int userToken; + + /** 用户经验值标记 */ + private int userExpFlag; + + /** 用户邮箱验证 */ + private int userEmailVerify; + + /** 用户重置密码验证 */ + private int userResetPWVerify; + + /** 访问频率控制 */ + private int rateLimit; + + /** 系统配置 */ + private int setting; + + /** Minecraft 登录 */ + private int fmcPlayerToken; + } + + @Override + protected RedisConfigParams configParams() { + return new RedisConfigParams() {{ + setHost(host); + setPort(port); + setPassword(password); + setTimeout(timeout); + setMaxActive(lettuce.pool.maxActive); + setMinIdle(lettuce.pool.minIdle); + setMaxIdle(lettuce.pool.maxIdle); + }}; + } + + /** @return 连接池配置 */ + @Bean + @Override + public GenericObjectPoolConfig getPoolConfig() { + GenericObjectPoolConfig config = new GenericObjectPoolConfig<>(); + config.setMaxTotal(lettuce.pool.maxActive); + config.setMinIdle(lettuce.pool.minIdle); + config.setMaxIdle(lettuce.pool.maxIdle); + config.setMaxWait(Duration.ofMillis(lettuce.pool.maxWait)); + return config; + } + + /** @return key 生成策略 */ + @Bean + @Override + public KeyGenerator keyGenerator() { + return (target, method, params) -> { + StringBuilder sb = new StringBuilder(); + sb.append(target.getClass().getName()); + sb.append(method.getName()); + for (Object obj : params) { + sb.append(obj.toString()); + } + return sb.toString(); + }; + } + + /** @return 分布式锁, ID: 尝试加锁次数 */ + @Bean("redisLocker") + public Redis getLockerRedisTemplate() { + return getRedis(database.locker, RedisSerializers.STRING, RedisSerializers.INTEGER); + } + + /** @return 多语言环境,ID: {@link Multilingual} */ + @Bean("redisLanguage") + public Redis getLanguageRedisTemplate() { + return getRedis(database.language, RedisSerializers.LONG, new RedisSerializer<>() { + + public Multilingual deserialize(@Nullable byte[] bytes) { + if (bytes == null || bytes.length == 0) { + return null; + } + try { + return (Multilingual) new DeserializingConverter().convert(bytes); + } catch (Exception var3) { + throw new SerializationException("Cannot deserialize", var3); + } + } + + public byte[] serialize(@Nullable Multilingual multilingual) { + if (multilingual == null) { + return new byte[0]; + } else { + try { + return new SerializingConverter().convert(multilingual); + } catch (Exception var3) { + throw new SerializationException("Cannot serialize", var3); + } + } + } + }); + } + + /** @return 文章访问记录,IP: [文章 ID] */ + @Bean("redisLanguageMap") + public Redis getLanguageMapRedisTemplate() { + return getRedis(database.languageMap, RedisSerializers.STRING, RedisSerializers.LONG); + } + + /** @return 文章访问统计,文章 ID: {@link ArticleRanking}(JSON) */ + @Bean("redisArticleRanking") + public Redis getArticleRankingRedisTemplate() { + return getRedis(database.articleRanking, RedisSerializers.LONG, RedisSerializers.gsonSerializer(ArticleRanking.class)); + } + + /** @return 文章访问记录,IP: [文章 ID] */ + @Bean("redisArticleRead") + public Redis getArticleReadRedisTemplate() { + return getRedis(database.articleRead, RedisSerializers.STRING, RedisSerializers.LONG); + } + + /** @return 用户登录经验标记,UID: NULL,暂时没有值,数据死亡时间为次日零时 */ + @Bean("redisUserExpFlag") + public Redis getUserExpFlagRedisTemplate() { + return getRedis(database.userExpFlag, RedisSerializers.LONG, RedisSerializers.STRING); + } + + /** @return 用户邮箱验证密钥,密钥: UID */ + @Bean("redisUserEmailVerify") + public Redis getUserEmailVerifyRedisTemplate() { + return getRedis(database.userEmailVerify, RedisSerializers.STRING, RedisSerializers.LONG); + } + + /** @return 用户重置密码密钥,密钥: UID */ + @Bean("redisUserResetPWVerify") + public Redis getUserResetPWVerifyRedisTemplate() { + return getRedis(database.userResetPWVerify, RedisSerializers.STRING, RedisSerializers.LONG); + } + + /** @return 接口访问控制,IP#方法: 生命周期内访问次数 */ + @Bean("redisRateLimit") + public Redis getRateLimitRedisTemplate() { + return getRedis(database.rateLimit, RedisSerializers.STRING, RedisSerializers.INTEGER); + } + + /** @return 系统配置,Key 枚举: String 配置值 */ + @Bean("redisSetting") + public Redis getSettingRedisTemplate() { + return getRedis(database.setting, RedisSerializers.STRING, RedisSerializers.STRING); + } + + /** @return Minecraft 登录,令牌: 玩家 ID */ + @Bean("redisMCPlayerToken") + public Redis getMCPlayerLoginRedisTemplate() { + return getRedis(database.fmcPlayerToken, RedisSerializers.STRING, RedisSerializers.LONG); + } +} diff --git a/src/main/java/com/imyeyu/server/config/SchedulerConfig.java b/src/main/java/com/imyeyu/server/config/SchedulerConfig.java new file mode 100644 index 0000000..e06d811 --- /dev/null +++ b/src/main/java/com/imyeyu/server/config/SchedulerConfig.java @@ -0,0 +1,33 @@ +package com.imyeyu.server.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +/** + * + * + * @author 夜雨 + * @since 2024-12-19 23:04 + */ +@Configuration +public class SchedulerConfig { + + @Bean + public TaskScheduler taskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(32); + scheduler.initialize(); + return scheduler; + } + + @Bean + public ScheduledTaskRegistrar scheduleCronTask(TaskScheduler taskScheduler) { + ScheduledTaskRegistrar registrar = new ScheduledTaskRegistrar(); + registrar.setTaskScheduler(taskScheduler); + registrar.afterPropertiesSet(); + return registrar; + } +} diff --git a/src/main/java/com/imyeyu/server/config/ThreadPoolConfig.java b/src/main/java/com/imyeyu/server/config/ThreadPoolConfig.java new file mode 100644 index 0000000..9cc9eed --- /dev/null +++ b/src/main/java/com/imyeyu/server/config/ThreadPoolConfig.java @@ -0,0 +1,57 @@ +package com.imyeyu.server.config; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 线程池配置 + * + * @author 夜雨 + * @since 2023-08-21 16:31 + */ +@Data +@Slf4j +@Configuration +@EnableAutoConfiguration +@ConfigurationProperties(prefix = "spring.async.thread-pool") +public class ThreadPoolConfig { + + /** 核心数量 */ + private int corePoolSize; + + /** 最大数量 */ + private int maxPoolSize; + + /** 等待区容量 */ + private int queueCapacity; + + /** 最大保持活跃时间(秒) */ + private int keepAliveSeconds; + + /** 最大等待时间(秒) */ + private int awaitTerminationSeconds; + + /** 线程名称前缀 */ + private String threadNamePrefix; + + @Bean(name = "threadPoolTaskExecutor") + public ThreadPoolTaskExecutor threadPoolTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(corePoolSize); + executor.setMaxPoolSize(maxPoolSize); + executor.setQueueCapacity(queueCapacity); + executor.setKeepAliveSeconds(keepAliveSeconds); + executor.setAwaitTerminationSeconds(awaitTerminationSeconds); + executor.setThreadNamePrefix(threadNamePrefix); + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + executor.initialize(); + return executor; + } +} diff --git a/src/main/java/com/imyeyu/server/config/WebConfig.java b/src/main/java/com/imyeyu/server/config/WebConfig.java new file mode 100644 index 0000000..c4ec2cd --- /dev/null +++ b/src/main/java/com/imyeyu/server/config/WebConfig.java @@ -0,0 +1,94 @@ +package com.imyeyu.server.config; + +import com.google.gson.GsonBuilder; +import com.imyeyu.server.annotation.EnableSettingInterceptor; +import com.imyeyu.server.annotation.RequestRateLimitInterceptor; +import com.imyeyu.server.annotation.RequiredTokenInterceptor; +import com.imyeyu.server.modules.common.entity.Attachment; +import com.imyeyu.server.modules.common.vo.user.UserProfileView; +import com.imyeyu.server.modules.common.vo.user.UserView; +import com.imyeyu.server.modules.minecraft.annotation.RequiredFMCServerTokenInterceptor; +import com.imyeyu.server.modules.minecraft.entity.MinecraftPlayer; +import com.imyeyu.server.modules.mirror.vo.MirrorView; +import com.imyeyu.server.modules.system.util.SystemAPIInterceptor; +import com.imyeyu.server.util.GsonSerializerAdapter; +import com.imyeyu.spring.annotation.RequestSingleParamResolver; +import jakarta.validation.constraints.NotNull; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.GsonHttpMessageConverter; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.io.Writer; +import java.lang.reflect.Type; +import java.util.List; + +/** + * 系统配置 + * + * @author 夜雨 + * @since 2021-07-20 16:44 + */ +@EnableWebMvc +@Configuration +@RequiredArgsConstructor +public class WebConfig implements WebMvcConfigurer { + + private final SystemAPIInterceptor systemAPIInterceptor; + private final GsonSerializerAdapter gsonSerializerAdapter; + private final RequiredTokenInterceptor requiredTokenInterceptor; + private final EnableSettingInterceptor enableSettingInterceptor; + private final RequestSingleParamResolver requestSingleParamResolver; + private final RequestRateLimitInterceptor requestRateLimitInterceptor; + private final RequiredFMCServerTokenInterceptor requiredFMCServerTokenInterceptor; + + /** + * 过滤器 + * + * @param registry 注册表 + */ + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(systemAPIInterceptor).addPathPatterns(SystemAPIInterceptor.PATH); + registry.addInterceptor(requiredFMCServerTokenInterceptor).addPathPatterns("/fmc/server/**"); + registry.addInterceptor(requiredTokenInterceptor).addPathPatterns("/**"); + registry.addInterceptor(enableSettingInterceptor).addPathPatterns("/**"); + registry.addInterceptor(requestRateLimitInterceptor).addPathPatterns("/**"); + } + + @Override + public void addArgumentResolvers(List argumentResolvers) { + argumentResolvers.add(requestSingleParamResolver); + } + + /** + * 通信消息转换 + * + * @param converters 转换器 + */ + @Override + public void configureMessageConverters(List> converters) { + GsonHttpMessageConverter converter = new GsonHttpMessageConverter() { + + @Override + protected void writeInternal(@NotNull Object object, Type type, @NonNull Writer writer) { + // 忽略参数类型,因为接口返回对象会被全局返回处理器包装为 TimiResponse,否则会序列化转型错误 + getGson().toJson(object, writer); + } + }; + + GsonBuilder builder = new GsonBuilder(); + builder.registerTypeAdapter(Attachment.class, gsonSerializerAdapter); + builder.registerTypeAdapter(UserView.class, gsonSerializerAdapter); + builder.registerTypeAdapter(MirrorView.class, gsonSerializerAdapter); + builder.registerTypeAdapter(UserProfileView.class, gsonSerializerAdapter); + builder.registerTypeAdapter(MinecraftPlayer.class, gsonSerializerAdapter); + converter.setGson(builder.create()); + converters.add(converter); + } +} diff --git a/src/main/java/com/imyeyu/server/config/dbsource/ForeverMCDBConfig.java b/src/main/java/com/imyeyu/server/config/dbsource/ForeverMCDBConfig.java new file mode 100644 index 0000000..2304f10 --- /dev/null +++ b/src/main/java/com/imyeyu/server/config/dbsource/ForeverMCDBConfig.java @@ -0,0 +1,76 @@ +package com.imyeyu.server.config.dbsource; + +import com.zaxxer.hikari.HikariDataSource; +import com.imyeyu.utils.Time; +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.mybatis.spring.SqlSessionTemplate; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; + +import javax.sql.DataSource; +import java.sql.SQLException; + +/** + * ForeverMC 登录校验数据源 + * + * @author 夜雨 + * @since 2022-11-29 22:39 + */ +@Configuration +@MapperScan(basePackages = "com.imyeyu.server.modules.forevermc.mapper", sqlSessionFactoryRef = "foreverMCSqlSessionFactory") +public class ForeverMCDBConfig { + + public static final String ROLLBACKER = "foreverMCTransactionManager"; + + @Bean(name = "foreverMCDataSource") + @Primary + @ConfigurationProperties(prefix = "spring.datasource.forevermc") + public DataSource getPrimaryDateSource() throws SQLException { + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setAutoCommit(true); + dataSource.setMinimumIdle(10); + dataSource.setMaximumPoolSize(100); + dataSource.setConnectionTestQuery("SELECT 1"); + + dataSource.setMaxLifetime(Time.S * 180); + dataSource.setIdleTimeout(Time.S * 120); + dataSource.setLoginTimeout(5); + dataSource.setValidationTimeout(Time.S * 3); + dataSource.setConnectionTimeout(Time.S * 8); + dataSource.setLeakDetectionThreshold(Time.S * 180); + return dataSource; + } + + + @Bean(name = "foreverMCSqlSessionFactory") + @Primary + public SqlSessionFactory primarySqlSessionFactory(@Qualifier("foreverMCDataSource") DataSource datasource) throws Exception { + org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration(); + config.setUseGeneratedKeys(true); + config.setMapUnderscoreToCamelCase(true); + + SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); + bean.setDataSource(datasource); + bean.setTypeAliasesPackage("com.imyeyu.server.modules.forevermc.entity"); + bean.setConfiguration(config); + return bean.getObject(); + } + + @Bean("foreverMCSqlSessionTemplate") + @Primary + public SqlSessionTemplate primarySqlSessionTemplate(@Qualifier("foreverMCSqlSessionFactory") SqlSessionFactory sessionfactory) { + return new SqlSessionTemplate(sessionfactory); + } + + @Bean(name = "foreverMCTransactionManager") + public PlatformTransactionManager txManager(@Qualifier("foreverMCDataSource") DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } +} diff --git a/src/main/java/com/imyeyu/server/config/dbsource/GiteaDBConfig.java b/src/main/java/com/imyeyu/server/config/dbsource/GiteaDBConfig.java new file mode 100644 index 0000000..aa7e747 --- /dev/null +++ b/src/main/java/com/imyeyu/server/config/dbsource/GiteaDBConfig.java @@ -0,0 +1,104 @@ +package com.imyeyu.server.config.dbsource; + +import com.imyeyu.utils.Time; +import com.zaxxer.hikari.HikariDataSource; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.type.EnumTypeHandler; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.mybatis.spring.SqlSessionTemplate; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; + +import javax.sql.DataSource; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * Gitea 数据源 + * + * @author 夜雨 + * @since 2022-11-29 22:40 + */ +@Configuration +@MapperScan(basePackages = { + "com.imyeyu.server.modules.gitea.mapper", +}, sqlSessionFactoryRef = "giteaSqlSessionFactory") +public class GiteaDBConfig { + + public static final String ROLLBACKER = "giteaTransactionManager"; + + @Bean(name = "giteaDataSource") + @Primary + @ConfigurationProperties(prefix = "spring.datasource.gitea") + public DataSource dateSource() throws SQLException { + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setAutoCommit(true); + dataSource.setMinimumIdle(10); + dataSource.setMaximumPoolSize(100); + dataSource.setConnectionTestQuery("SELECT 1"); + + dataSource.setMaxLifetime(Time.S * 180); + dataSource.setIdleTimeout(Time.S * 120); + dataSource.setLoginTimeout(5); + dataSource.setValidationTimeout(Time.S * 3); + dataSource.setConnectionTimeout(Time.S * 8); + dataSource.setLeakDetectionThreshold(Time.S * 180); + return dataSource; + } + + + @Bean(name = "giteaSqlSessionFactory") + @Primary + public SqlSessionFactory sessionFactory(@Qualifier("giteaDataSource") DataSource datasource) throws Exception { + org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration(); + config.setUseGeneratedKeys(true); + config.setMapUnderscoreToCamelCase(true); + config.setDefaultEnumTypeHandler(EnumTypeHandler.class); + + List resources = new ArrayList<>(); + { + ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver(); + List mapperLocations = new ArrayList<>(); + mapperLocations.add("classpath:mapper/gitea/**/*.xml"); + for (int i = 0; i < mapperLocations.size(); i++) { + resources.addAll(List.of(resourceResolver.getResources(mapperLocations.get(i)))); + } + } + String[] typeAliases = { + "com.imyeyu.server.modules.gitea.entity", + }; + String[] typeHandlers = { + "com.imyeyu.server.handler" + }; + + SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); + bean.setDataSource(datasource); + bean.setConfiguration(config); + bean.setMapperLocations(resources.toArray(new Resource[0])); + bean.setTypeAliasesPackage(String.join(",", typeAliases)); + bean.setTypeHandlersPackage(String.join(",", typeHandlers)); + return bean.getObject(); + } + + + @Bean("giteaSqlSessionTemplate") + @Primary + public SqlSessionTemplate sessionTemplate(@Qualifier("giteaSqlSessionFactory") SqlSessionFactory sessionfactory) { + return new SqlSessionTemplate(sessionfactory); + } + + @Bean(name = "giteaTransactionManager") + public PlatformTransactionManager txManager(@Qualifier("giteaDataSource") DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } +} diff --git a/src/main/java/com/imyeyu/server/config/dbsource/TimiServerDBConfig.java b/src/main/java/com/imyeyu/server/config/dbsource/TimiServerDBConfig.java new file mode 100644 index 0000000..4eb4059 --- /dev/null +++ b/src/main/java/com/imyeyu/server/config/dbsource/TimiServerDBConfig.java @@ -0,0 +1,122 @@ +package com.imyeyu.server.config.dbsource; + +import com.zaxxer.hikari.HikariDataSource; +import com.imyeyu.utils.Time; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.type.EnumTypeHandler; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.mybatis.spring.SqlSessionTemplate; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; + +import javax.sql.DataSource; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * TimiServer 数据源 + * + * @author 夜雨 + * @since 2022-11-29 22:40 + */ +@Configuration +@MapperScan(basePackages = { + "com.imyeyu.server.modules.git.mapper", + "com.imyeyu.server.modules.bill.mapper", + "com.imyeyu.server.modules.blog.mapper", + "com.imyeyu.server.modules.lyric.mapper", + "com.imyeyu.server.modules.mirror.mapper", + "com.imyeyu.server.modules.system.mapper", + "com.imyeyu.server.modules.common.mapper", + "com.imyeyu.server.modules.minecraft.mapper" +}, sqlSessionFactoryRef = "timiServerSqlSessionFactory") +public class TimiServerDBConfig { + + public static final String ROLLBACKER = "timiServerTransactionManager"; + + @Bean(name = "timiServerDataSource") + @Primary + @ConfigurationProperties(prefix = "spring.datasource.timiserver") + public DataSource getPrimaryDateSource() throws SQLException { + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setAutoCommit(true); + dataSource.setMinimumIdle(10); + dataSource.setMaximumPoolSize(100); + dataSource.setConnectionTestQuery("SELECT 1"); + + dataSource.setMaxLifetime(Time.S * 180); + dataSource.setIdleTimeout(Time.S * 120); + dataSource.setLoginTimeout(5); + dataSource.setValidationTimeout(Time.S * 3); + dataSource.setConnectionTimeout(Time.S * 8); + dataSource.setLeakDetectionThreshold(Time.S * 180); + return dataSource; + } + + + @Bean(name = "timiServerSqlSessionFactory") + @Primary + public SqlSessionFactory primarySqlSessionFactory(@Qualifier("timiServerDataSource") DataSource datasource) throws Exception { + org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration(); + config.setUseGeneratedKeys(true); + config.setMapUnderscoreToCamelCase(true); + config.setDefaultEnumTypeHandler(EnumTypeHandler.class); + + List resources = new ArrayList<>(); + { + ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver(); + List mapperLocations = new ArrayList<>(); + mapperLocations.add("classpath:mapper/git/**/*.xml"); + mapperLocations.add("classpath:mapper/blog/**/*.xml"); + mapperLocations.add("classpath:mapper/common/**/*.xml"); + mapperLocations.add("classpath:mapper/system/**/*.xml"); + mapperLocations.add("classpath:mapper/minecraft/**/*.xml"); + for (int i = 0; i < mapperLocations.size(); i++) { + resources.addAll(List.of(resourceResolver.getResources(mapperLocations.get(i)))); + } + } + String[] typeAliases = { + "com.imyeyu.server.modules.git.entity", + "com.imyeyu.server.modules.bill.entity", + "com.imyeyu.server.modules.blog.entity", + "com.imyeyu.server.modules.lyric.entity", + "com.imyeyu.server.modules.mirror.entity", + "com.imyeyu.server.modules.system.entity", + "com.imyeyu.server.modules.common.entity", + "com.imyeyu.server.modules.minecraft.entity" + }; + String[] typeHandlers = { + "com.imyeyu.server.handler" + }; + + SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); + bean.setDataSource(datasource); + bean.setConfiguration(config); + bean.setMapperLocations(resources.toArray(new Resource[0])); + bean.setTypeAliasesPackage(String.join(",", typeAliases)); + bean.setTypeHandlersPackage(String.join(",", typeHandlers)); + return bean.getObject(); + } + + + @Bean("timiServerSqlSessionTemplate") + @Primary + public SqlSessionTemplate primarySqlSessionTemplate(@Qualifier("timiServerSqlSessionFactory") SqlSessionFactory sessionfactory) { + return new SqlSessionTemplate(sessionfactory); + } + + @Bean(name = "timiServerTransactionManager") + public PlatformTransactionManager txManager(@Qualifier("timiServerDataSource") DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } +} diff --git a/src/main/java/com/imyeyu/server/handler/GsonHandler.java b/src/main/java/com/imyeyu/server/handler/GsonHandler.java new file mode 100644 index 0000000..49afd15 --- /dev/null +++ b/src/main/java/com/imyeyu/server/handler/GsonHandler.java @@ -0,0 +1,57 @@ +package com.imyeyu.server.handler; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.imyeyu.java.TimiJava; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * MySQL JSON 数据类型处理器 + * + * @author 夜雨 + * @since 2021-07-04 09:36 + */ +public class GsonHandler extends BaseTypeHandler { + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, JsonElement parameter, JdbcType jdbcType) throws SQLException { + ps.setString(i, String.valueOf(parameter.toString())); + } + + @Override + public JsonElement getNullableResult(ResultSet rs, String columnName) throws SQLException { + return toElement(rs.getString(columnName)); + } + + @Override + public JsonElement getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + return toElement(rs.getString(columnIndex)); + } + + @Override + public JsonElement getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + return toElement(cs.getNString(columnIndex)); + } + + private JsonElement toElement(String json) { + if (TimiJava.isNotEmpty(json)) { + JsonElement el = JsonParser.parseString(json); + if (el.isJsonObject()) { + return el.getAsJsonObject(); + } + if (el.isJsonArray()) { + return el.getAsJsonArray(); + } + if (el.isJsonPrimitive()) { + return el.getAsJsonPrimitive(); + } + } + return null; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/bill/controller/BillController.java b/src/main/java/com/imyeyu/server/modules/bill/controller/BillController.java new file mode 100644 index 0000000..1880fd4 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/bill/controller/BillController.java @@ -0,0 +1,49 @@ +package com.imyeyu.server.modules.bill.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.bill.entity.Bill; +import com.imyeyu.server.modules.bill.service.BillService; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.spring.TimiSpring; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 收支帐单接口 + * + * @author 夜雨 + * @since 2023-02-04 01:02 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/bill") +public class BillController { + + private final BillService service; + private final SettingService settingService; + + /** + * 创建收支帐单 + * + * @param bill 账单 + */ + @AOPLog + @RequestRateLimit + @PostMapping("/create") + public void createREBill(@Valid @RequestBody Bill bill) { + if (!settingService.getAsString(SettingKey.BILL_API_TOKEN).equals(TimiSpring.getToken())) { + throw new TimiException(TimiCode.REQUEST_BAD).msgKey("token.illegal"); + } + service.create(bill); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/bill/entity/Bill.java b/src/main/java/com/imyeyu/server/modules/bill/entity/Bill.java new file mode 100644 index 0000000..4896c40 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/bill/entity/Bill.java @@ -0,0 +1,120 @@ +package com.imyeyu.server.modules.bill.entity; + +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import com.imyeyu.spring.entity.Entity; + +/** + * 收支账单 + * + * @author 夜雨 + * @since 2022-03-29 11:28 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class Bill extends Entity { + + /** + * 类型 + * + * @author 夜雨 + * @since 2022-03-29 11:28 + */ + @Getter + public enum Type { + + /** 收入 */ + REVENUE, + + /** 支出 */ + EXPENDITURE + } + + /** + * 收入类型 + * + * @author 夜雨 + * @since 2022-03-29 11:28 + */ + @Getter + public enum RevenueType { + + /** 工作 */ + WORK, + + /** 退款 */ + REFUND, + + /** 其他 */ + OTHER + } + + /** + * 支出类型 + * + * @author 夜雨 + * @since 2022-03-29 11:28 + */ + @Getter + public enum ExpenditureType { + + /** 饮食 */ + FOOD, + + /** 生活 */ + LIFE, + + /** 通信 */ + COMMUNICATION, + + /** 交通 */ + TRAFFIC, + + /** 娱乐 */ + GAME, + + /** 工作 */ + WORK, + + /** 服饰 */ + CLOTHES, + + /** 医疗 */ + HEALTH, + + /** 其他 */ + OTHER + } + + /** 收入类型 */ + private RevenueType revenueType; + + /** 支出类型 */ + private ExpenditureType expenditureType; + + /** 描述 */ + @NotBlank(message = "bill.description.empty") + private String description; + + /** 金额(未确保计算精度,放大了 100 倍) */ + @NotNull(message = "bill.decimal.empty") + @DecimalMin(value = "0", message = "bill.decimal.limit") + private Long decimal; + + /** 备注 */ + private String remarks; + + /** @return true 为收入账单 */ + public boolean isRevenue() { + return revenueType != null; + } + + /** @return true 为支出账单 */ + public boolean isExpenditure() { + return expenditureType != null; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/bill/mapper/BillMapper.java b/src/main/java/com/imyeyu/server/modules/bill/mapper/BillMapper.java new file mode 100644 index 0000000..75a7be3 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/bill/mapper/BillMapper.java @@ -0,0 +1,13 @@ +package com.imyeyu.server.modules.bill.mapper; + +import com.imyeyu.server.modules.bill.entity.Bill; +import com.imyeyu.spring.mapper.BaseMapper; + +/** + * 收支帐单表 + * + * @author 夜雨 + * @since 2022-04-01 16:26 + */ +public interface BillMapper extends BaseMapper { +} diff --git a/src/main/java/com/imyeyu/server/modules/bill/service/BillService.java b/src/main/java/com/imyeyu/server/modules/bill/service/BillService.java new file mode 100644 index 0000000..82a6771 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/bill/service/BillService.java @@ -0,0 +1,13 @@ +package com.imyeyu.server.modules.bill.service; + +import com.imyeyu.server.modules.bill.entity.Bill; +import com.imyeyu.spring.service.CreatableService; + +/** + * 收支帐单服务 + * + * @author 夜雨 + * @since 2022-04-01 16:24 + */ +public interface BillService extends CreatableService { +} diff --git a/src/main/java/com/imyeyu/server/modules/bill/service/implement/BillServiceImplement.java b/src/main/java/com/imyeyu/server/modules/bill/service/implement/BillServiceImplement.java new file mode 100644 index 0000000..26aa7e4 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/bill/service/implement/BillServiceImplement.java @@ -0,0 +1,27 @@ +package com.imyeyu.server.modules.bill.service.implement; + +import com.imyeyu.server.modules.bill.entity.Bill; +import com.imyeyu.server.modules.bill.mapper.BillMapper; +import com.imyeyu.server.modules.bill.service.BillService; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 收支账单服务 + * + * @author 夜雨 + * @since 2022-04-01 16:25 + */ +@Service +@RequiredArgsConstructor +public class BillServiceImplement extends AbstractEntityService implements BillService { + + private final BillMapper mapper; + + @Override + protected BaseMapper mapper() { + return mapper; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/blog/controller/ArticleController.java b/src/main/java/com/imyeyu/server/modules/blog/controller/ArticleController.java new file mode 100644 index 0000000..b3c4b39 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/blog/controller/ArticleController.java @@ -0,0 +1,93 @@ +package com.imyeyu.server.modules.blog.controller; + +import com.imyeyu.server.modules.blog.entity.Article; +import com.imyeyu.server.modules.blog.entity.ArticleRanking; +import com.imyeyu.server.modules.blog.service.ArticleService; +import com.imyeyu.server.modules.blog.vo.article.ArticleView; +import com.imyeyu.server.modules.blog.vo.article.KeywordPage; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.bean.Page; +import com.imyeyu.spring.bean.PageResult; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 文章接口 + * + * @author 夜雨 + * @since 2021-02-17 17:47 + */ +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/article") +public class ArticleController { + + private final ArticleService service; + + /** + * 查看 + * + * @param id 文章 ID + * @return 文章 + */ + @AOPLog + @RequestRateLimit + @RequestMapping("/{id}") + public ArticleView view(@Min(1) @NotNull @PathVariable Long id) { + return service.view(id); + } + + /** + * 喜欢文章 + * + * @param id 文章 ID + * @return 最新喜欢数量 + */ + @AOPLog + @RequestRateLimit + @RequestMapping("/like/{id}") + public int like(@Min(1) @NotNull @PathVariable Long id) { + return service.like(id); + } + + /** + * 主列表 + * + * @param page 页面参数 + * @return 文章列表 + */ + @RequestRateLimit + @RequestMapping("/list") + public PageResult

list(@Valid @RequestBody Page page) { + return service.page(page); + } + + /** + * 根据关键字获取列表 + * + * @param page 关键字页面参数 + * @return 文章列表 + */ + @RequestRateLimit + @RequestMapping("/list/search") + public PageResult
listByKeyword(@Valid @RequestBody KeywordPage page) { + return service.pageByKeyword(page); + } + + /** @return 每周访问排位 */ + @RequestMapping("/list/ranking") + public List ranking() { + return service.listRanking(); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/blog/controller/BlogController.java b/src/main/java/com/imyeyu/server/modules/blog/controller/BlogController.java new file mode 100644 index 0000000..a5302ee --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/blog/controller/BlogController.java @@ -0,0 +1,32 @@ +package com.imyeyu.server.modules.blog.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.server.modules.blog.entity.Friend; +import com.imyeyu.server.modules.blog.service.FriendService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 主控 + * + * @author 夜雨 + * @since 2023-02-04 10:28 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/") +public class BlogController { + + private final FriendService friendService; + + /** @return 所有友链列表 */ + @GetMapping("/friend") + public List friend() { + return friendService.listAll(); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/blog/entity/Article.java b/src/main/java/com/imyeyu/server/modules/blog/entity/Article.java new file mode 100644 index 0000000..e4a7f31 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/blog/entity/Article.java @@ -0,0 +1,95 @@ +package com.imyeyu.server.modules.blog.entity; + +import com.google.gson.JsonElement; +import com.imyeyu.server.modules.common.bean.CommentSupport; +import com.imyeyu.spring.entity.Destroyable; +import com.imyeyu.spring.entity.Entity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 文章 + * + * @author 夜雨 + * @since 2021-03-01 17:10 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class Article extends Entity implements CommentSupport, Destroyable { + + /** + * 文章渲染类型,对应前端模板 + * + * @author 夜雨 + * @since 2021-07-04 09:23 + */ + public enum Type { + + /** 关于 */ + ABOUT, + + /** 公版 */ + PUBLIC, + + /** 音乐 */ + MUSIC, + + /** 软件 */ + SOFTWARE + } + + /** 标题 */ + protected String title; + + /** 类型 */ + protected Type type; + + /** 分类 ID */ + protected long classId; + + /** 摘要 */ + protected String digest; + + /** 数据 */ + protected String data; + + /** 扩展数据 */ + protected JsonElement extendData; + + /** 阅读数量 */ + protected int reads; + + /** 喜欢数量 */ + protected int likes; + + /** 是否显示评论 */ + protected boolean showComment; + + /** true 为可评论 */ + protected boolean canComment; + + /** true 为可排位 */ + protected boolean canRanking; + + /** @return true 为可评论 */ + @Override + public boolean canComment() { + return canComment; + } + + /** @return true 为不可评论 */ + @Override + public boolean canNotComment() { + return !canComment; + } + + /** @return true 为可排位 */ + public boolean canRanking() { + return canRanking; + } + + /** @return true 为不可排位 */ + public boolean canNotRanking() { + return !canRanking; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/blog/entity/ArticleRanking.java b/src/main/java/com/imyeyu/server/modules/blog/entity/ArticleRanking.java new file mode 100644 index 0000000..01f2e1d --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/blog/entity/ArticleRanking.java @@ -0,0 +1,33 @@ +package com.imyeyu.server.modules.blog.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.entity.Entity; + +/** + * 访问排行(每周) + * 只记录访问次数、标题和最近访问,具体文章由 Redis key 记录 + * + * @author 夜雨 + * @since 2021-03-01 17:10 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ArticleRanking extends Entity { + + private String title; + private Article.Type type; + private int count = 1; + private Long recentAt; // 最近访问 + + public ArticleRanking(Long id, String title, Article.Type type) { + setId(id); + this.title = title; + this.type = type; + } + + /** 访问计数 + 1 */ + public void read() { + count++; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/blog/entity/CommentRemindQueue.java b/src/main/java/com/imyeyu/server/modules/blog/entity/CommentRemindQueue.java new file mode 100644 index 0000000..e3a0b9d --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/blog/entity/CommentRemindQueue.java @@ -0,0 +1,36 @@ +package com.imyeyu.server.modules.blog.entity; + +import com.imyeyu.server.modules.common.vo.comment.CommentReplyView; +import com.imyeyu.spring.annotation.table.AutoUUID; +import com.imyeyu.spring.annotation.table.Id; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 评论回复提醒队列,和 CommentReplyRecord 不一样,本队列在推送消息后就删除,而后者会持久保存 + * + *

基本逻辑: + *

+ *     触发:用户回复一条评论
+ *     条件:被回复者是注册用户 && 不是回复自己 && 邮箱已验证 && 接收回复提醒邮件
+ *     事件:添加本对象到队列列表,等待邮件推送服务调度,邮件推送服务
+ * 
+ * 会针对用户收集本队列消息组合成邮件再一并推送 + * + * @author 夜雨 + * @since 2021-08-25 00:00 + */ +@Data +@NoArgsConstructor +public class CommentRemindQueue { + + @Id + @AutoUUID + private String UUID; + + private Long userId; + + private Long replyId; + + private CommentReplyView reply; +} diff --git a/src/main/java/com/imyeyu/server/modules/blog/entity/Friend.java b/src/main/java/com/imyeyu/server/modules/blog/entity/Friend.java new file mode 100644 index 0000000..5a0d550 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/blog/entity/Friend.java @@ -0,0 +1,21 @@ +package com.imyeyu.server.modules.blog.entity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import com.imyeyu.spring.entity.Entity; + +/** + * 夜雨 创建于 2021-07-15 15:59 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Friend extends Entity { + + private String icon; + private String name; + private String link; +} diff --git a/src/main/java/com/imyeyu/server/modules/blog/mapper/ArticleMapper.java b/src/main/java/com/imyeyu/server/modules/blog/mapper/ArticleMapper.java new file mode 100644 index 0000000..c5840c8 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/blog/mapper/ArticleMapper.java @@ -0,0 +1,26 @@ +package com.imyeyu.server.modules.blog.mapper; + +import com.imyeyu.server.modules.blog.entity.Article; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 文章 + * + * @author 夜雨 + * @since 2021-02-23 21:34 + */ +public interface ArticleMapper extends BaseMapper { + + long countByKeyword(String keyword); + + List
selectByKeyword(String keyword, Long offset, int limit); + + @Select("UPDATE `article` SET `likes` = `likes` + 1 WHERE `id` = #{articleId}") + void like(Long articleId); + + @Select("UPDATE `article` SET `reads` = `reads` + 1 WHERE `id` = #{articleId}") + void read(Long articleId); +} diff --git a/src/main/java/com/imyeyu/server/modules/blog/mapper/FriendMapper.java b/src/main/java/com/imyeyu/server/modules/blog/mapper/FriendMapper.java new file mode 100644 index 0000000..4de7854 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/blog/mapper/FriendMapper.java @@ -0,0 +1,20 @@ +package com.imyeyu.server.modules.blog.mapper; + + +import com.imyeyu.server.modules.blog.entity.Friend; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 友链 + * + * @author 夜雨 + * @since 2021-07-15 16:11 + */ +public interface FriendMapper extends BaseMapper { + + @Select("SELECT * FROM friend WHERE 1 = 1" + NOT_DELETE) + List listAll(); +} diff --git a/src/main/java/com/imyeyu/server/modules/blog/service/ArticleService.java b/src/main/java/com/imyeyu/server/modules/blog/service/ArticleService.java new file mode 100644 index 0000000..421d912 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/blog/service/ArticleService.java @@ -0,0 +1,48 @@ +package com.imyeyu.server.modules.blog.service; + +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.blog.entity.Article; +import com.imyeyu.server.modules.blog.entity.ArticleRanking; +import com.imyeyu.server.modules.blog.vo.article.ArticleView; +import com.imyeyu.server.modules.blog.vo.article.KeywordPage; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.GettableService; +import com.imyeyu.spring.service.PageableService; + +import java.util.List; + +/** + * 文章服务 + * + * @author 夜雨 + * @since 2021-02-23 21:33 + */ +public interface ArticleService extends GettableService, PageableService
{ + + /** + * 获取文章,此方法触发阅读计数,包括触发每周热门排行统计,同一 IP 3 小时内访问多次的文章只计一次 + * + * @param id 文章 ID + * @throws TimiException 服务异常 + */ + ArticleView view(long id); + + PageResult
pageByKeyword(KeywordPage page); + + /** + * 获取每周阅读排行 + * + * @return 热门文章列表 + * @throws TimiException 服务异常 + */ + List listRanking(); + + /** + * 喜欢文章 + * + * @param articleId 文章 ID + * @return 最新喜欢数量 + * @throws TimiException 服务异常 + */ + int like(Long articleId); +} diff --git a/src/main/java/com/imyeyu/server/modules/blog/service/CommentRemindQueueService.java b/src/main/java/com/imyeyu/server/modules/blog/service/CommentRemindQueueService.java new file mode 100644 index 0000000..1eb4eb0 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/blog/service/CommentRemindQueueService.java @@ -0,0 +1,41 @@ +package com.imyeyu.server.modules.blog.service; + +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.blog.entity.CommentRemindQueue; +import com.imyeyu.spring.service.CreatableService; + +import java.util.List; + +/** + * 评论回复队列服务 + * + * @author 夜雨 + * @since 2021-08-25 00:11 + */ +public interface CommentRemindQueueService extends CreatableService { + + /** + * 根据用户 ID 获取 + * + * @param userId 用户 ID + * @return 回复提醒列表 + * @throws TimiException 服务异常 + */ + List listByUserId(Long userId); + + /** + * 根据用户 ID 移出队列 + * + * @param uid 用户 ID + * @throws TimiException 服务异常 + */ + void destroyByUserId(Long uid); + + /** + * 根据回复 ID 移出队列 + * + * @param rid 回复 ID + * @throws TimiException 服务异常 + */ + void destroyByReplyId(Long rid); +} diff --git a/src/main/java/com/imyeyu/server/modules/blog/service/FriendService.java b/src/main/java/com/imyeyu/server/modules/blog/service/FriendService.java new file mode 100644 index 0000000..d50b14b --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/blog/service/FriendService.java @@ -0,0 +1,16 @@ +package com.imyeyu.server.modules.blog.service; + +import com.imyeyu.server.modules.blog.entity.Friend; + +import java.util.List; + +/** + * 友链服务 + * + * @author 夜雨 + * @since 2021-07-15 16:04 + */ +public interface FriendService { + + List listAll(); +} diff --git a/src/main/java/com/imyeyu/server/modules/blog/service/implement/ArticleServiceImplement.java b/src/main/java/com/imyeyu/server/modules/blog/service/implement/ArticleServiceImplement.java new file mode 100644 index 0000000..5a27f83 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/blog/service/implement/ArticleServiceImplement.java @@ -0,0 +1,113 @@ +package com.imyeyu.server.modules.blog.service.implement; + +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.config.dbsource.TimiServerDBConfig; +import com.imyeyu.server.modules.blog.entity.Article; +import com.imyeyu.server.modules.blog.entity.ArticleRanking; +import com.imyeyu.server.modules.blog.mapper.ArticleMapper; +import com.imyeyu.server.modules.blog.service.ArticleService; +import com.imyeyu.server.modules.blog.vo.article.ArticleView; +import com.imyeyu.server.modules.blog.vo.article.KeywordPage; +import com.imyeyu.server.modules.common.entity.Attachment; +import com.imyeyu.server.modules.common.entity.Comment; +import com.imyeyu.server.modules.common.entity.Tag; +import com.imyeyu.server.modules.common.mapper.CommentMapper; +import com.imyeyu.server.modules.common.service.AttachmentService; +import com.imyeyu.server.modules.common.service.TagService; +import com.imyeyu.spring.TimiSpring; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.spring.util.Redis; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Comparator; +import java.util.List; + +/** + * 文章服务实现 + * + * @author 夜雨 + * @since 2021-02-17 17:48 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ArticleServiceImplement extends AbstractEntityService implements ArticleService { + + private final TagService tagService; + private final AttachmentService attachmentService; + + private final ArticleMapper mapper; + private final CommentMapper commentMapper; + + private final Redis redisArticleRead; + private final Redis redisArticleRanking; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public ArticleView view(long id) { + String ip = TimiSpring.getRequestIP(); + Article article = get(id); + TimiException.required(article, "article.not_found"); + // 计数 + if (!redisArticleRead.contains(ip, article.getId())) { + // 3 小时内访问记录 + redisArticleRead.add(ip, article.getId()); + redisArticleRead.setExpire(ip, Time.H * 3); + mapper.read(article.getId()); + // 每周访问计数 + if (article.canRanking()) { + ArticleRanking ranking = redisArticleRanking.get(article.getId()); + if (ranking == null) { + ranking = new ArticleRanking(article.getId(), article.getTitle(), article.getType()); + ranking.setRecentAt(Time.now()); + redisArticleRanking.set(article.getId(), ranking, Time.D * 7); + } else { + ranking.read(); + ranking.setRecentAt(Time.now()); + redisArticleRanking.setAndKeepTTL(article.getId(), ranking); + } + } + } + + ArticleView view = new ArticleView(); + BeanUtils.copyProperties(article, view); + view.setComments(commentMapper.countAll(Comment.BizType.ARTICLE, article.getId())); + view.setTagList(tagService.listByBizID(Tag.BizType.ARTICLE, String.valueOf(article.getId()))); + view.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.ARTICLE, article.getId())); + return view; + } + + @Override + public PageResult
pageByKeyword(KeywordPage page) { + PageResult
result = new PageResult<>(); + result.setList(mapper.selectByKeyword(page.getKeyword(), page.getOffset(), page.getLimit())); + result.setTotal(mapper.countByKeyword(page.getKeyword())); + return result; + } + + @Override + public List listRanking() { + List list = redisArticleRanking.values(); + list.sort(Comparator.comparing(ArticleRanking::getCount).reversed()); + return list.subList(0, Math.min(10, list.size())); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public int like(Long articleId) { + mapper.like(articleId); + return get(articleId).getLikes(); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/blog/service/implement/CommentRemindQueueServiceImplement.java b/src/main/java/com/imyeyu/server/modules/blog/service/implement/CommentRemindQueueServiceImplement.java new file mode 100644 index 0000000..c18fa54 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/blog/service/implement/CommentRemindQueueServiceImplement.java @@ -0,0 +1,48 @@ +package com.imyeyu.server.modules.blog.service.implement; + +import com.imyeyu.server.config.dbsource.TimiServerDBConfig; +import com.imyeyu.server.modules.blog.entity.CommentRemindQueue; +import com.imyeyu.server.modules.blog.service.CommentRemindQueueService; +import com.imyeyu.server.modules.common.mapper.CommentRemindQueueMapper; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 评论回复队列服务实现 + * + * @author 夜雨 + * @since 2021-08-25 00:11 + */ +@Service +@RequiredArgsConstructor +public class CommentRemindQueueServiceImplement extends AbstractEntityService implements CommentRemindQueueService { + + private final CommentRemindQueueMapper mapper; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public List listByUserId(Long userId) { + return mapper.listByUserId(userId); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void destroyByUserId(Long userId) { + mapper.destroyByUserId(userId); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void destroyByReplyId(Long replyId) { + mapper.destroyByReplyId(replyId); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/blog/service/implement/FriendServiceImplement.java b/src/main/java/com/imyeyu/server/modules/blog/service/implement/FriendServiceImplement.java new file mode 100644 index 0000000..d02c9c8 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/blog/service/implement/FriendServiceImplement.java @@ -0,0 +1,27 @@ +package com.imyeyu.server.modules.blog.service.implement; + +import lombok.RequiredArgsConstructor; +import com.imyeyu.server.modules.blog.entity.Friend; +import com.imyeyu.server.modules.blog.mapper.FriendMapper; +import com.imyeyu.server.modules.blog.service.FriendService; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 友链服务实现 + * + * @author 夜雨 + * @since 2021-07-15 16:05 + */ +@Service +@RequiredArgsConstructor +public class FriendServiceImplement implements FriendService { + + private final FriendMapper mapper; + + @Override + public List listAll() { + return mapper.listAll(); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/blog/util/UserToken.java b/src/main/java/com/imyeyu/server/modules/blog/util/UserToken.java new file mode 100644 index 0000000..7e67ba5 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/blog/util/UserToken.java @@ -0,0 +1,148 @@ +package com.imyeyu.server.modules.blog.util; + +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.config.RedisConfig; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.entity.User; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.spring.TimiSpring; +import com.imyeyu.spring.util.Redis; +import com.imyeyu.spring.util.RedisSerializers; +import com.imyeyu.utils.Time; +import jakarta.annotation.Nullable; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.Cookie; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +/** + * Redis 令牌缓存 + * + *

一级缓存 Session,二级缓存 Redis,有效期为 {@link SettingKey#TTL_USER_TOKEN} 天,每次触发 + * 二级缓存获取时会刷新这个时间,即指定天数内不再访问则视为登出 + * + * @author 夜雨 + * @since 2023-07-17 16:58 + */ +@Component +@RequiredArgsConstructor(onConstructor_ = {@Lazy}) +public class UserToken { + + private final RedisConfig redisConfig; + + private final UserService userService; + private final SettingService settingService; + + private Redis redis; + + @PostConstruct + private void postConstruct() { + redis = redisConfig.getRedis(redisConfig.getDatabase().getUserToken(), RedisSerializers.STRING, RedisSerializers.LONG); + } + + public Long set(String token, Long userId) { + long ttl = Time.D * settingService.getAsInt(SettingKey.TTL_USER_TOKEN); + // 会话 + TimiSpring.setSessionAttr(token, userId); + // 跨站 Cookie + Cookie cookie = Objects.requireNonNullElse(TimiSpring.getCookie("Token"), new Cookie("Token", token)); + cookie.setDomain(settingService.getAsString(SettingKey.DOMAIN_ROOT)); + cookie.setPath("/"); + cookie.setSecure(true); + cookie.setMaxAge((int) (ttl / 1000)); + TimiSpring.addCookie(cookie); + // Redis + redis.set(token, userId, ttl); + return Time.now() + ttl; + } + + public Long getExpireAt(String token) { + return Time.now() + redis.getExpire(token); + } + + /** + * 获取令牌是否有效 + * + * @param token 令牌 + * @return true 为有效 + */ + public boolean isValid(String token) { + return getUserId(token) != null; + } + + /** + * 获取令牌是否无效({@link #isValid(String)} 取反) + * + * @param token 令牌 + * @return true 为无效 + */ + public boolean isInvalid(String token) { + return !isValid(token); + } + + /** + * 获取用户 ID + *

一级缓存 Session,二级缓存 Redis,每次触发二级缓存获取时会刷新这个时间 + *

Session 存键为 token,值为 UserId,Redis 也相同 + * + * @param token 令牌 + * @return 用户 ID + * @throws TimiException 无效 token + */ + public @Nullable Long getRequiredUserId(String token) throws TimiException { + return TimiException.required(getUserId(token), "invalid token"); + } + + /** + * 获取用户 ID + *

一级缓存 Session,二级缓存 Redis,每次触发二级缓存获取时会刷新这个时间 + *

Session 存键为 token,值为 UserId,Redis 也相同 + * + * @param token 令牌 + * @return 用户 ID,token 无效时为 null + */ + public @Nullable Long getUserId(String token) { + if (TimiJava.isEmpty(token)) { + return null; + } + Long userId; + // Session + if (TimiSpring.getSessionAttr(token) instanceof Long sessionUserId) { + userId = sessionUserId; + } else { + // Redis + userId = redis.get(token); + } + if (TimiJava.isEmpty(userId)) { + return null; + } + // 刷新 + set(token, userId); + return userId; + } + + public @NotNull User getUser(String token) { + return userService.get(getRequiredUserId(token)); + } + + public void clear(String token) { + // 会话 + TimiSpring.removeSessionAttr(token); + // 清除跨站 Cookie + Cookie cookie = new Cookie("Token", "DIED"); + cookie.setDomain(settingService.getAsString(SettingKey.DOMAIN_ROOT)); + cookie.setPath("/"); + cookie.setSecure(true); + cookie.setMaxAge(0); + TimiSpring.addCookie(cookie); + // Redis + redis.destroy(token); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/blog/vo/article/ArticleView.java b/src/main/java/com/imyeyu/server/modules/blog/vo/article/ArticleView.java new file mode 100644 index 0000000..8fb3f98 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/blog/vo/article/ArticleView.java @@ -0,0 +1,24 @@ +package com.imyeyu.server.modules.blog.vo.article; + +import com.imyeyu.server.modules.blog.entity.Article; +import com.imyeyu.server.modules.common.entity.Attachment; +import com.imyeyu.server.modules.common.entity.Tag; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2023-08-07 17:19 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ArticleView extends Article { + + private long comments; + + private List tagList; + + private List attachmentList; +} diff --git a/src/main/java/com/imyeyu/server/modules/blog/vo/article/ClassPage.java b/src/main/java/com/imyeyu/server/modules/blog/vo/article/ClassPage.java new file mode 100644 index 0000000..64c5e8b --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/blog/vo/article/ClassPage.java @@ -0,0 +1,16 @@ +package com.imyeyu.server.modules.blog.vo.article; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.bean.Page; + +/** + * @author 夜雨 + * @since 2023-07-14 17:52 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ClassPage extends Page { + + private long classId; +} diff --git a/src/main/java/com/imyeyu/server/modules/blog/vo/article/KeywordPage.java b/src/main/java/com/imyeyu/server/modules/blog/vo/article/KeywordPage.java new file mode 100644 index 0000000..ab5c063 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/blog/vo/article/KeywordPage.java @@ -0,0 +1,18 @@ +package com.imyeyu.server.modules.blog.vo.article; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.bean.Page; + +/** + * @author 夜雨 + * @since 2023-07-14 17:53 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class KeywordPage extends Page { + + @NotBlank(message = "article.keyword.empty") + private String keyword; +} diff --git a/src/main/java/com/imyeyu/server/modules/blog/vo/article/LabelPage.java b/src/main/java/com/imyeyu/server/modules/blog/vo/article/LabelPage.java new file mode 100644 index 0000000..d916ac1 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/blog/vo/article/LabelPage.java @@ -0,0 +1,16 @@ +package com.imyeyu.server.modules.blog.vo.article; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.bean.Page; + +/** + * @author 夜雨 + * @since 2023-07-14 17:53 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class LabelPage extends Page { + + private long labelId; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/bean/CommentSupport.java b/src/main/java/com/imyeyu/server/modules/common/bean/CommentSupport.java new file mode 100644 index 0000000..099d23d --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/bean/CommentSupport.java @@ -0,0 +1,16 @@ +package com.imyeyu.server.modules.common.bean; + +/** + * 支持评论的实体 + * + * @author 夜雨 + * @since 2023-10-10 11:39 + */ +public interface CommentSupport { + + /** @return true 为可评论 */ + boolean canComment(); + + /** @return true 为不可评论 */ + boolean canNotComment(); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/bean/EmailException.java b/src/main/java/com/imyeyu/server/modules/common/bean/EmailException.java new file mode 100644 index 0000000..05fb34d --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/bean/EmailException.java @@ -0,0 +1,30 @@ +package com.imyeyu.server.modules.common.bean; + +import lombok.Getter; +import lombok.Setter; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; + +/** + * 邮件服务异常 + * + * @author 夜雨 + * @since 2021-10-03 11:14 + */ +public class EmailException extends TimiException { + + /** 邮箱 */ + @Setter + @Getter + private String email; + + public EmailException(TimiCode code, String email) { + super(code); + this.email = email; + } + + public EmailException(TimiCode code, String msg, String email) { + super(code, msg); + this.email = email; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/bean/ImageType.java b/src/main/java/com/imyeyu/server/modules/common/bean/ImageType.java new file mode 100644 index 0000000..adeb9c9 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/bean/ImageType.java @@ -0,0 +1,18 @@ +package com.imyeyu.server.modules.common.bean; + +/** + * + * @author 夜雨 + * @since 2021-09-20 11:49 + */ +public enum ImageType { + + /** 双线性 */ + AUTO, + + /** 模糊 */ + SMOOTH, + + /** 像素 */ + PIXELATED +} diff --git a/src/main/java/com/imyeyu/server/modules/common/bean/SettingKey.java b/src/main/java/com/imyeyu/server/modules/common/bean/SettingKey.java new file mode 100644 index 0000000..40047f6 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/bean/SettingKey.java @@ -0,0 +1,156 @@ +package com.imyeyu.server.modules.common.bean; + +/** + * 系统设置 + * + * @author 夜雨 + * @since 2021-07-20 21:46 + */ +public enum SettingKey { + + // ---------- 通用 ---------- + + RUN_ENV, + + PUBLIC_RESOURCES, + + /** 启用注册 */ + ENABLE_REGISTER, + + /** 启用登录 */ + ENABLE_LOGIN, + + /** 启用评论 */ + ENABLE_COMMENT, + + /** 启用测试 */ + ENABLE_DEBUG, + + /** 启用账号数据更新(User 和 UserProfile) */ + ENABLE_USER_UPDATE, + + /** 启用灰色滤镜 */ + ENABLE_GRAY_FILTER, + + // ---------- ICP 备案号 ---------- + + ICP_IMYEYU_COM, + + // ---------- 域名 ---------- + + DOMAIN_ROOT, + + DOMAIN_API, + + DOMAIN_GIT, + + DOMAIN_BLOG, + + DOMAIN_SPACE, + + DOMAIN_RESOURCE, + + DOMAIN_DOWNLOAD, + + DOMAIN_FOREVER_MC, + + // ---------- ForeverMC ---------- + + /** 启用登录服务 */ + FMC_PLAYER_LOGIN_ENABLE, + + /** 最多绑定玩家数量 */ + FMC_MAX_BIND, + + /** 闪烁标语 */ + FMC_SPLASHES, + + /** 启动器背景 */ + FMC_BG, + + FMC_BGM, + + FMC_BG_SWIPER, + + /** JRE 列表 */ + FMC_JRE, + + /** 辅助登录模组 */ + FMC_LOGIN_FABRIC, + + /** 启用图片地图上传 */ + FMC_ENABLE_IMAGE_MAP_UPLOAD, + + /** 玩家登录令牌有效期(天) */ + FMC_PLAYER_LOGIN_TOKEN_TTL, + + /** 服务器与数据中心的通信令牌 */ + FMC_SERVER_TOKEN, + + // ---------- 生存时间 ---------- + + TTL_USER_TOKEN, + + TTL_SETTING, + + TTL_MULTILINGUAL, + + // ---------- 多语言翻译 ---------- + + MULTILINGUAL_TRANSLATE_API, + + MULTILINGUAL_TRANSLATE_APP_ID, + + MULTILINGUAL_TRANSLATE_KEY, + + // ---------- 账单 ---------- + + BILL_API_TOKEN, + + // ---------- Git ---------- + + GIT_API, + + // ---------- 远程音乐 ---------- + + MUSIC_MAX_FRAME_LENGTH, + + MUSIC_PLAYER_PORT, + + MUSIC_PLAYER_IP, + + MUSIC_CONTROLLER_PORT, + + MUSIC_CONTROLLER_IP, + + MUSIC_CONTROLLER_URI, + + // ---------- 系统 ---------- + + SYSTEM_FILE_BASE, + + SYSTEM_FILE_TYPE, + + SYSTEM_FILE_SYNC, + + /** 文件过滤(通过密钥类型) */ + SYSTEM_FILE_FILTER, + + SYSTEM_STATUS_RATE, + + SYSTEM_STATUS_LIMIT, + + SYSTEM_STATUS_NETWORK_MAC, + + SYSTEM_TERMINAL_TTL, + + SYSTEM_TERMINAL_FILTERS, + + /** 一般密钥 */ + SYSTEM_API_KEY, + + /** 超级密钥 */ + SYSTEM_API_SUPER_KEY, + + SYSTEM_REBOOT_COMMAND, +} diff --git a/src/main/java/com/imyeyu/server/modules/common/controller/CommentController.java b/src/main/java/com/imyeyu/server/modules/common/controller/CommentController.java new file mode 100644 index 0000000..5fe5ff8 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/controller/CommentController.java @@ -0,0 +1,85 @@ +package com.imyeyu.server.modules.common.controller; + +import com.imyeyu.server.annotation.CaptchaValid; +import com.imyeyu.server.annotation.EnableSetting; +import com.imyeyu.server.bean.CaptchaFrom; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.entity.Comment; +import com.imyeyu.server.modules.common.entity.CommentReply; +import com.imyeyu.server.modules.common.service.CommentReplyService; +import com.imyeyu.server.modules.common.service.CommentService; +import com.imyeyu.server.modules.common.vo.comment.CommentReplyPage; +import com.imyeyu.server.modules.common.vo.comment.CommentReplyView; +import com.imyeyu.server.modules.common.vo.comment.CommentView; +import com.imyeyu.server.modules.git.vo.issue.CommentPage; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.bean.CaptchaData; +import com.imyeyu.spring.bean.PageResult; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 评论操作接口 + *

*评论回复只依赖评论而不含业务关联,评论有关联业务,所以此接口是通用接口 + * + * @author 夜雨 + * @since 2021-02-23 21:36 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/comment") +public class CommentController { + + private final CommentService service; + private final CommentReplyService replyService; + + @AOPLog + @CaptchaValid(CaptchaFrom.COMMENT) + @EnableSetting(value = SettingKey.ENABLE_COMMENT, message = "comment.off_service") + @RequestRateLimit + @PostMapping("/create") + public void create(@Valid @RequestBody CaptchaData captchaData) { + service.create(captchaData.getData()); + } + + @RequestRateLimit + @PostMapping("/list") + public PageResult list(@Valid @RequestBody CommentPage commentPage) { + return service.pageByBizId(commentPage); + } + + /** + * 创建评论回复 + * + * @param request 回复数据 + */ + @AOPLog + @CaptchaValid(CaptchaFrom.COMMENT_REPLY) + @EnableSetting(value = SettingKey.ENABLE_COMMENT, message = "comment.off_service") + @RequestRateLimit + @PostMapping("/reply/create") + public void createReply(@Valid @RequestBody CaptchaData request) { + replyService.create(request.getData()); + } + + /** + * 获取回复列表 + * + * @param page 页面参数 + * @return 回复列表 + */ + @RequestRateLimit + @RequestMapping("/reply/list") + public PageResult pageCommentReplies(@Valid @RequestBody CommentReplyPage page) { + // 通用接口,只允许查询评论的回复 + page.setBizType(CommentReplyPage.BizType.COMMENT); + return replyService.pageByBizType(page); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/controller/CommonController.java b/src/main/java/com/imyeyu/server/modules/common/controller/CommonController.java new file mode 100644 index 0000000..91532a2 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/controller/CommonController.java @@ -0,0 +1,375 @@ +package com.imyeyu.server.modules.common.controller; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.imyeyu.io.IO; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.java.ref.Ref; +import com.imyeyu.network.Network; +import com.imyeyu.server.bean.CaptchaFrom; +import com.imyeyu.server.modules.common.bean.ImageType; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.entity.Attachment; +import com.imyeyu.server.modules.common.entity.Setting; +import com.imyeyu.server.modules.common.entity.Task; +import com.imyeyu.server.modules.common.entity.Template; +import com.imyeyu.server.modules.common.entity.Version; +import com.imyeyu.server.modules.common.service.AttachmentService; +import com.imyeyu.server.modules.common.service.FeedbackService; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.server.modules.common.service.TaskService; +import com.imyeyu.server.modules.common.service.TemplateService; +import com.imyeyu.server.modules.common.service.VersionService; +import com.imyeyu.server.modules.common.vo.FeedbackRequest; +import com.imyeyu.server.modules.common.vo.attachment.AttachmentView; +import com.imyeyu.server.modules.system.util.ResourceHandler; +import com.imyeyu.server.util.CaptchaManager; +import com.imyeyu.spring.TimiSpring; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.IgnoreGlobalReturn; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.bean.CaptchaData; +import com.mongodb.client.gridfs.GridFSBucket; +import com.mongodb.client.gridfs.GridFSDownloadStream; +import com.mongodb.client.gridfs.model.GridFSFile; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.tika.Tika; +import org.springframework.data.mongodb.gridfs.GridFsResource; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.yaml.snakeyaml.Yaml; + +import javax.imageio.ImageIO; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 系统接口 + * + * @author 夜雨 + * @since 2021-02-23 21:38 + */ +@Slf4j +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/") +public class CommonController { + + private final TaskService taskService; + private final VersionService versionService; + private final SettingService settingService; + private final FeedbackService feedbackService; + private final TemplateService templateService; + private final AttachmentService attachmentService; + + private final Gson gson; + private final Yaml yaml; + private final GridFSBucket gridFSBucket; + private final CaptchaManager captchaManager; + private final ResourceHandler resourceHandler; + + @AOPLog + @RequestMapping("") + public String root() { + return "IT WORKING! " + TimiSpring.getRequestIP(); + } + + /** + * 获取验证码 + * + * @param width 宽度 + * @param height 高度 + * @param from 来源 + * @param response 返回对象 + */ + @IgnoreGlobalReturn + @GetMapping("/captcha") + public void captcha(int width, int height, CaptchaFrom from, HttpServletResponse response) { + // 返回图像流 + response.setHeader("Pragma", "no-cache"); + response.setHeader("Cache-Control", "no-cache"); // 禁止缓存 + response.setDateHeader("Expires", -1); + response.setContentType("image/jpg"); + try { + // 宽度 + if (width < 64) { + ImageIO.write(captchaManager.error(TimiCode.ARG_BAD), "jpg", response.getOutputStream()); + return; + } + // 高度 + if (height < 19) { + ImageIO.write(captchaManager.error(TimiCode.ARG_BAD), "jpg", response.getOutputStream()); + return; + } + // 来自 + if (TimiJava.isEmpty(from)) { + ImageIO.write(captchaManager.error(TimiCode.ARG_MISS), "jpg", response.getOutputStream()); + return; + } + // 输出图像流 + ImageIO.write(captchaManager.generate(from, width, height), "jpg", response.getOutputStream()); + } catch (Exception e) { + log.error("CommonController.getCaptcha", e); + try { + ImageIO.write(captchaManager.error(TimiCode.ERROR), "jpg", response.getOutputStream()); + } catch (IOException subE) { + log.error("write error image fail", subE); + } + } + } + + /** + * 获取软件最新版本状态 + * + * @param name 软件名称 + * @return 最新版本状态 + * @deprecated 兼容旧程序 + */ + @AOPLog + @GetMapping("/versions/{name}") + @Deprecated + public Version versions(@Valid @NotBlank @PathVariable("name") String name) { + return versionService.getByName(name); + } + + /** + * 获取软件最新版本状态 + * + * @param name 软件名称 + * @return 最新版本状态 + */ + @AOPLog + @GetMapping("/version/{name}") + public Version version(@Valid @NotBlank @PathVariable("name") String name) { + return versionService.getByName(name); + } + + @AOPLog + @RequestRateLimit + @PostMapping("/feedback") + public void createFeedback(@Valid @NotNull @RequestBody CaptchaData request) { + captchaManager.test(request.getCaptcha(), request.getFrom()); + feedbackService.create(request.getData()); + } + + /** @return 公开任务信息 */ + @AOPLog + @RequestRateLimit + @GetMapping("/tasklist") + public List getTasks() { + return taskService.listAll4Public(); + } + + @RequestRateLimit + @GetMapping("/template") + public String viewTemplate(@RequestParam Template.BizType bizType, @RequestParam String bizCode) { + return templateService.get(bizType, bizCode).getData(); + } + + @RequestRateLimit + @GetMapping("/setting/{key}") + public String settingByKey(@PathVariable("key") String key, @RequestParam(value = "as", required = false) Setting.Type asType) { + Setting setting = settingService.getByKey(SettingKey.valueOf(key.toUpperCase())); + if (setting.isPrivate()) { + throw new TimiException(TimiCode.PERMISSION_ERROR); + } + String result = setting.getValue(); + if (asType == null) { + return result; + } + switch (asType) { + case JSON -> { + if (setting.getType() == Setting.Type.YAML) { + Map obj = yaml.load(setting.getValue()); + result = gson.toJson(obj); + } + } + case YAML -> { + if (setting.getType() == Setting.Type.JSON) { + Map obj = gson.fromJson(setting.getValue(), new TypeToken>() {}.getType()); + result = yaml.dump(obj); + } + } + } + return result; + } + + @RequestRateLimit + @PostMapping("/setting/map") + public Map mapSettingByKeys(@RequestBody Map> settingMap) { + List result = settingService.listByKeys(new ArrayList<>(settingMap.keySet())); + for (int i = 0; i < result.size(); i++) { + Setting setting = result.get(i); + if (setting.isPrivate()) { + throw new TimiException(TimiCode.PERMISSION_ERROR); + } + Map args = settingMap.get(setting.getKey()); + if (args == null) { + continue; + } + if (args.containsKey("as")) { + switch (Ref.toType(Setting.Type.class, args.get("as").toString())) { + case JSON -> { + if (setting.getType() == Setting.Type.YAML) { + Map obj = new Yaml().load(setting.getValue()); + setting.setValue(gson.toJson(obj)); + } + } + case YAML -> { + if (setting.getType() == Setting.Type.JSON) { + Map obj = gson.fromJson(setting.getValue(), new TypeToken>() {}.getType()); + setting.setValue(new Yaml().dump(obj)); + } + } + } + } + } + return result.stream().collect(Collectors.toMap(Setting::getKey, Setting::getValue)); + } + + @RequestRateLimit + @GetMapping("/setting/flushCache") + public void settingFlushCache() { + settingService.flushCache(); + } + + @AOPLog + @RequestRateLimit + @GetMapping("/attachment/{mongoId}") + public AttachmentView getAttachment(@PathVariable String mongoId) { + return attachmentService.viewByMongoId(mongoId); + } + + @AOPLog + @RequestRateLimit + @IgnoreGlobalReturn + @GetMapping("/attachment/read/{mongoId}") + public void readAttachment( + @PathVariable String mongoId, + @RequestParam(name = "size", required = false) Integer size, + @RequestParam(name = "type", required = false) String type, + HttpServletRequest req, + HttpServletResponse resp + ) { + try { + GridFSFile file = attachmentService.readByMongoId(mongoId); + if (file == null) { + resp.setStatus(HttpServletResponse.SC_NOT_FOUND); + resp.setCharacterEncoding(StandardCharsets.UTF_8.toString()); + return; + } + GridFSDownloadStream mimeReadStream = gridFSBucket.openDownloadStream(file.getObjectId()); + String mimeType = new Tika().detect(mimeReadStream); + if (TimiJava.isNotEmpty(mimeType)) { + resp.setContentType(mimeType); + } + if (size != null) { + String fileType = switch (mimeType) { + case "image/png" -> "png"; + case "image/jpeg" -> "jpg"; + default -> throw new TimiException(TimiCode.ARG_BAD).msgKey("TODO not support Re render mineType"); + }; + + switch (mimeType) { + case "image/png", "image/jpeg" -> { + // 图片缩放 + GridFSDownloadStream stream = gridFSBucket.openDownloadStream(file.getObjectId()); + byte[] bytes = IO.toBytes(stream); + BufferedImage imgSrc = ImageIO.read(new ByteArrayInputStream(bytes)); + + double scale; + if (imgSrc.getHeight() < imgSrc.getWidth()) { + // 横向 + scale = 1D * size / imgSrc.getWidth(); + } else { + scale = 1D * size / imgSrc.getHeight(); + } + int width = (int) (imgSrc.getWidth() * scale); + int height = (int) (imgSrc.getHeight() * scale); + BufferedImage imgResult = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics2D g = imgResult.createGraphics(); + if (ImageType.PIXELATED == Ref.toType(ImageType.class, type)) { + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + } else { + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); + g.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); + g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + } + g.drawImage(imgSrc, 0, 0, width, height, null); + ImageIO.write(imgResult, fileType, resp.getOutputStream()); + } + default -> throw new TimiException(TimiCode.ARG_BAD).msgKey("TODO not support Re render mineType"); + } + } else { + GridFSDownloadStream downloadStream = gridFSBucket.openDownloadStream(file.getObjectId()); + GridFsResource gridFsResource = new GridFsResource(file, downloadStream); + req.setAttribute(ResourceHandler.ATTR_TYPE, ResourceHandler.Type.MONGO); + req.setAttribute(ResourceHandler.ATTR_VALUE, gridFsResource); + resourceHandler.handleRequest(req, resp); + } + } catch (Exception e) { + log.error("read attachment error", e); + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + resp.setCharacterEncoding(StandardCharsets.UTF_8.toString()); + } + } + + @AOPLog + @RequestRateLimit + @IgnoreGlobalReturn + @GetMapping("/attachment/download/{mongoId}") + public void downloadAttachment(@PathVariable String mongoId, HttpServletResponse resp) { + try { + Attachment attachment = attachmentService.getByMongoId(mongoId); + GridFSFile file = attachmentService.readByMongoId(mongoId); + if (file == null) { + resp.setStatus(HttpServletResponse.SC_NOT_FOUND); + resp.setCharacterEncoding(StandardCharsets.UTF_8.toString()); + return; + } + { + GridFSDownloadStream downloadStream = gridFSBucket.openDownloadStream(file.getObjectId()); + String mimeType = new Tika().detect(downloadStream); + if (TimiJava.isNotEmpty(mimeType)) { + resp.setContentType(mimeType); + } + } + GridFSDownloadStream downloadStream = gridFSBucket.openDownloadStream(file.getObjectId()); + resp.setHeader("Content-Disposition", Network.getFileDownloadHeader(file.getFilename())); + resp.setHeader("Content-Range", String.valueOf(attachment.getSize())); + resp.setHeader("Accept-Ranges", "bytes"); + resp.setContentLengthLong(attachment.getSize()); + IO.toOutputStream(downloadStream, resp.getOutputStream()); + resp.flushBuffer(); + } catch (Exception e) { + log.error("read attachment error", e); + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + resp.setCharacterEncoding(StandardCharsets.UTF_8.toString()); + } + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/controller/IconController.java b/src/main/java/com/imyeyu/server/modules/common/controller/IconController.java new file mode 100644 index 0000000..39bfd7c --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/controller/IconController.java @@ -0,0 +1,97 @@ +package com.imyeyu.server.modules.common.controller; + +import com.imyeyu.server.modules.common.entity.Icon; +import com.imyeyu.server.modules.common.service.IconService; +import com.imyeyu.server.modules.common.vo.icon.AllResponse; +import com.imyeyu.server.modules.common.vo.icon.NamePage; +import com.imyeyu.server.modules.common.vo.icon.UnicodePage; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.bean.Page; +import com.imyeyu.spring.bean.PageResult; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * timi-icon 前端接口 + * + * @author 夜雨 + * @since 2022-09-14 23:59 + */ +@Slf4j +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/icon") +public class IconController { + + private final IconService service; + + /** + * 获取图标列表 + * + * @param page 查询列表参数 + * @return 图标列表 + */ + @RequestRateLimit + @PostMapping("/list") + public PageResult list(@RequestBody Page page) { + return service.page(page); + } + + /** + * 获取所有图标,为了减小传输数据,此接口只返回 name 名称、Unicode 代码和 SVG 路径 + * + * @param latest 请求缓存的最新数据时间 + * @return 所有图标,如果请求缓存的最新时间等于数据库的最新数据时间,不返回任何数据 + */ + @RequestRateLimit + @GetMapping("/list/all") + public AllResponse listAll(@Valid @RequestParam Long latest) { + AllResponse resp = service.listAll(latest); + List icons = resp.getIcons(); + for (int i = 0; i < icons.size(); i++) { + Icon icon = icons.get(i); + icon.setId(null); + icon.setCreatedAt(null); + icon.setUpdatedAt(null); + } + return resp; + } + + /** + * 根据名称获取查询列表参数列表 + * + * @param page 查询列表参数 + * @return 图标列表 + */ + @AOPLog + @RequestRateLimit + @PostMapping("/list/name") + public PageResult listByName(@Valid @RequestBody NamePage page) { + return service.pageByName(page); + } + + /** + * 根据 Unicode 获取图标列表 + * + * @param page 查询列表参数 + * @return 图标列表 + */ + @AOPLog + @RequestRateLimit + @PostMapping("/list/unicode") + public PageResult listByUnicode(@Valid @RequestBody UnicodePage page) { + return service.pageByUnicode(page); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/controller/UserController.java b/src/main/java/com/imyeyu/server/modules/common/controller/UserController.java new file mode 100644 index 0000000..96825dd --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/controller/UserController.java @@ -0,0 +1,315 @@ +package com.imyeyu.server.modules.common.controller; + +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.annotation.CaptchaValid; +import com.imyeyu.server.annotation.EnableSetting; +import com.imyeyu.server.bean.CaptchaFrom; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.entity.CommentReply; +import com.imyeyu.server.modules.common.entity.UserConfig; +import com.imyeyu.server.modules.common.entity.UserPrivacy; +import com.imyeyu.server.modules.common.service.CommentReplyService; +import com.imyeyu.server.modules.common.service.CommentService; +import com.imyeyu.server.modules.common.service.UserConfigService; +import com.imyeyu.server.modules.common.service.UserPrivacyService; +import com.imyeyu.server.modules.common.service.UserProfileService; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.server.modules.common.vo.comment.CommentReplyPage; +import com.imyeyu.server.modules.common.vo.comment.CommentReplyView; +import com.imyeyu.server.modules.common.vo.comment.CommentView; +import com.imyeyu.server.modules.common.vo.comment.UserCommentPage; +import com.imyeyu.server.modules.common.vo.user.EmailVerifyCallbackRequest; +import com.imyeyu.server.modules.common.vo.user.LoginRequest; +import com.imyeyu.server.modules.common.vo.user.LoginResponse; +import com.imyeyu.server.modules.common.vo.user.RegisterRequest; +import com.imyeyu.server.modules.common.vo.user.UpdatePasswordByKeyRequest; +import com.imyeyu.server.modules.common.vo.user.UpdatePasswordRequest; +import com.imyeyu.server.modules.common.vo.user.UserRequest; +import com.imyeyu.server.modules.common.vo.user.UserView; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.annotation.RequestSingleParam; +import com.imyeyu.spring.annotation.RequiredToken; +import com.imyeyu.spring.bean.CaptchaData; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.utils.Time; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 用户接口 + * + * @author 夜雨 + * @since 2021-02-23 21:38 + */ +@Slf4j +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/user") +public class UserController implements TimiJava { + + private final UserService service; + private final CommentService commentService; + private final UserConfigService configService; + private final UserProfileService profileService; + private final UserPrivacyService privacyService; + private final CommentReplyService commentReplyService; + + /** + * 注册。执行成功会自动登录 + * + * @param request 注册请求 + * @return 登录数据 + */ + @AOPLog + @CaptchaValid(CaptchaFrom.REGISTER) + @EnableSetting(value = SettingKey.ENABLE_REGISTER, message = "user.register.off_service") + @RequestRateLimit(value = 1, lifeCycle = 60) + @PostMapping("/register") + public LoginResponse register(@Valid @RequestBody CaptchaData request) { + return service.register(request.getData()); + } + + /** + * 登录 + * + * @param request 登录请求 + * @return 登录数据 + */ + @AOPLog + @CaptchaValid(CaptchaFrom.LOGIN) + @EnableSetting(value = SettingKey.ENABLE_LOGIN, message = "user.login.off_service") + @RequestRateLimit + @PostMapping("/login") + public LoginResponse login(@Valid @RequestBody CaptchaData request) { + return service.login(request.getData()); + } + + /** + * 根据令牌登录,请求头携带 Token 参数,通常用于延续登录令牌 + * + * @return 登录数据 + */ + @AOPLog + @EnableSetting(value = SettingKey.ENABLE_LOGIN, message = "user.login.off_service") + @RequestRateLimit + @PostMapping("/login/token") + public LoginResponse login4Token() { + return service.login4Token(); + } + + /** 登出 */ + @AOPLog + @RequestRateLimit + @PostMapping("/logout") + public void logout() { + service.logout(); + } + + /** 发送邮箱验证邮件 */ + @AOPLog + @RequiredToken + @RequestRateLimit(value = 1, lifeCycle = 50) + @PostMapping("/email/verify") + public void sendEmailVerify() { + service.sendEmailVerify(); + } + + /** + * 邮箱验证邮件回调,验证请求的密钥来源于 {@link #sendEmailVerify()} 接口发送的邮件 + * + * @param request 邮箱验证请求 + */ + @AOPLog + @RequiredToken + @RequestRateLimit(value = 1, lifeCycle = 50) + @PostMapping("/email/verify/callback") + public void emailVerifyCallback(@Valid @RequestBody EmailVerifyCallbackRequest request) { + service.emailVerifyCallback(request.getKey()); + } + + /** + * 修改密码,需要已登录状态,使用旧密码修改 + * + * @param request 修改密码请求 + */ + @AOPLog + @RequiredToken + @RequestRateLimit + @PostMapping("/password/update") + public void updatePassword(@Valid @RequestBody UpdatePasswordRequest request) { + service.updatePassword(request.getOldValue(), request.getNewValue()); + } + + /** + * 发送用于重置密码的忘记密码邮件,入参数据可能是 UID、邮箱或用户名,该数据目标用户的邮箱需要通过验证 + * + * @param request 忘记密码邮件请求 + */ + @AOPLog + @CaptchaValid(CaptchaFrom.RESET_PASSWORD) + @RequestRateLimit(value = 1, lifeCycle = 50) + @PostMapping("/password/forget") + public void sendPasswordForgetVerify(@Valid @RequestBody CaptchaData request) { + service.sendPasswordForgetVerify(request.getData()); + } + + /** + * 修改密码,不需要登录状态,入参数据的密钥来源于 {@link #sendPasswordForgetVerify(CaptchaData)} 接口发送的邮件 + * + * @param request 重置密码请求 + */ + @AOPLog + @RequestRateLimit(value = 1, lifeCycle = 50) + @PostMapping("/password/reset") + public void resetPasswordByKey(@Valid @RequestBody UpdatePasswordByKeyRequest request) { + service.resetPasswordByKey(request.getKey(), request.getNewPassword()); + } + + /** + * 注销账号,此操作将会标记此用户的所有数据为删除状态 + * + * @param password 密码 + */ + @AOPLog + @RequiredToken + @RequestRateLimit + @PostMapping("/cancel") + public void cancel(@RequestSingleParam String password) { + service.cancel(password); + } + + /** + * 获取用户资料 + * + * @param userId 目标用户 ID + * @return 用户资料 + */ + @AOPLog + @RequestRateLimit + @PostMapping("/view/{userId}") + public UserView view(@Min(1) @NotNull @PathVariable Long userId) throws Exception { + return service.view(userId).doFilter(); + } + + /** + * 更新用户数据 + * + * @param data 用户数据(包括账号数据) + */ + @AOPLog + @RequiredToken + @EnableSetting(value = SettingKey.ENABLE_USER_UPDATE, message = "user.data.off_service") + @RequestRateLimit + @PostMapping("/profile/update") + public void updateProfile(@Valid UserRequest data) { + profileService.update(data); + } + + /** + * 获取用户隐私控制 + * + * @return 用户资料 + */ + @AOPLog + @RequiredToken + @RequestRateLimit + @PostMapping("/privacy") + public UserPrivacy privacy() { + return privacyService.get(service.getLoginUser().getId()); + } + + @AOPLog + @RequiredToken + @RequestRateLimit + @PostMapping("/privacy/update") + public void updatePrivacy(@Valid @RequestBody UserPrivacy privacy) { + privacyService.update(privacy); + } + + /** + * 获取用户设置 + * + * @return 用户设置 + */ + @AOPLog + @RequiredToken + @RequestRateLimit + @PostMapping("/config") + public UserConfig config() { + return configService.get(service.getLoginUser().getId()); + } + + @AOPLog + @RequiredToken + @RequestRateLimit + @PostMapping("/config/update") + public void updateConfig(@Valid @RequestBody UserConfig config) { + configService.update(config); + } + + /** + * 获取用户评论 + */ + @AOPLog + @RequiredToken + @RequestRateLimit + @PostMapping("/comment/list") + public PageResult listComment(@Valid @RequestBody UserCommentPage page) { + page.setUserId(service.getLoginUser().getId()); + return commentService.pageByUserId(page); + } + + @AOPLog + @RequiredToken + @RequestRateLimit + @PostMapping("/comment/delete") + public void deleteComment(@RequestSingleParam Long commentId) { + commentService.get(commentId); + commentService.delete(commentId); + } + + /** + * 获取用户被回复的评论 + */ + @AOPLog + @RequiredToken + @RequestRateLimit + @PostMapping("/comment/reply/list") + public PageResult listCommentReply(@Valid @RequestBody CommentReplyPage page) { + page.setBizId(service.getLoginUser().getId()); + return commentReplyService.pageByBizType(page); + } + + @AOPLog + @RequiredToken + @RequestRateLimit + @PostMapping("/comment/reply/delete") + public void deleteCommentReply(@RequestSingleParam Long replyId) { + CommentReply reply = commentReplyService.get(replyId); + TimiException.requiredTrue(reply.getSenderId().equals(service.getLoginUser().getId()), "user.comment.reply.delete.not_owner"); + commentReplyService.delete(replyId); + } + + @AOPLog + @RequiredToken + @RequestRateLimit + @PostMapping("/comment/reply/ignore") + public void ignoreCommentReply(@RequestSingleParam Long replyId) { + CommentReply reply = commentReplyService.get(replyId); + TimiException.requiredTrue(reply.getReceiverId().equals(service.getLoginUser().getId()), "user.comment.reply.ignore.not_owner"); + reply.setIgnoredAt(Time.now()); + commentReplyService.update(reply); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/entity/Attachment.java b/src/main/java/com/imyeyu/server/modules/common/entity/Attachment.java new file mode 100644 index 0000000..b233546 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/entity/Attachment.java @@ -0,0 +1,74 @@ +package com.imyeyu.server.modules.common.entity; + +import com.imyeyu.java.ref.Ref; +import com.imyeyu.server.bean.MultilingualHandler; +import com.imyeyu.spring.entity.Entity; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +/** + * @author 夜雨 + * @since 2023-08-15 10:17 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class Attachment extends Entity implements MultilingualHandler { + + /** + * 附件类型 + * + * @author 夜雨 + * @since 2023-08-21 16:32 + */ + @Getter + @AllArgsConstructor + public enum BizType { + + /** 用户 */ + USER, + + /** 文章 */ + ARTICLE, + + /** Git */ + GIT, + + /** 歌词 */ + LYRIC, + + /** ForeverMC */ + FMC, + + /** 镜像 */ + MIRROR, + + /** 系统 */ + SYSTEM + } + + private BizType bizType; + + private Long bizId; + + private String attachType; + + private String mongoId; + + @MultilingualField + private String title; + + private String name; + + private Long size; + + public void setAttachTypeValue(Enum attachType) { + this.attachType = attachType.toString(); + } + + public > T getAttachTypeValue(Class attachTypeClass) { + return Ref.toType(attachTypeClass, attachType); + } + +} diff --git a/src/main/java/com/imyeyu/server/modules/common/entity/Comment.java b/src/main/java/com/imyeyu/server/modules/common/entity/Comment.java new file mode 100644 index 0000000..f5348bb --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/entity/Comment.java @@ -0,0 +1,68 @@ +package com.imyeyu.server.modules.common.entity; + +import com.imyeyu.spring.service.GettableService; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import com.imyeyu.server.modules.blog.service.implement.ArticleServiceImplement; +import com.imyeyu.server.modules.common.bean.CommentSupport; +import com.imyeyu.server.modules.git.service.implement.IssueServiceImplement; +import com.imyeyu.server.modules.git.service.implement.MergeServiceImplement; +import com.imyeyu.spring.entity.Entity; +import com.imyeyu.spring.service.BaseService; + +/** + * 评论 + * + * @author 夜雨 + * @since 2021-02-25 14:46 + */ +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Comment extends Entity { + + + /** + * 关联业务类型 + *

+ * TODO 添加模块名称以便区别邮件通知推送来源,使用多语言键 + * + * @author 夜雨 + * @since 2023-08-06 23:42 + */ + @Getter + @AllArgsConstructor + public enum BizType { + + ARTICLE(ArticleServiceImplement.class), + + GIT_ISSUE(IssueServiceImplement.class), + + GIT_MERGE(MergeServiceImplement.class); + + final Class> serviceClass; + } + + /** 关联业务类型 */ + private BizType bizType; + + /** 关联业务 ID */ + private Long bizId; + + /** 发送用户 ID,登录用户评论有值,游客无 */ + private Long userId; + + /** 发送用户昵称,游客评论有值,登录用户无 */ + private String nick; + + /** 评论数据 */ + @NotBlank + private String content; + + /** 发送用户 IP */ + private String ip; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/entity/CommentReply.java b/src/main/java/com/imyeyu/server/modules/common/entity/CommentReply.java new file mode 100644 index 0000000..bcd98f5 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/entity/CommentReply.java @@ -0,0 +1,45 @@ +package com.imyeyu.server.modules.common.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import com.imyeyu.spring.entity.Entity; + +/** + * 评论回复 + * + * @author 夜雨 + * @since 2021-03-01 17:11 + */ +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class CommentReply extends Entity { + + /** 所属评论 ID */ + private Long commentId; + + /** 被回复的回复,回复主评论时为 NULL */ + private Long replyId; + + /** 发送用户 ID,登录用户回复有值,游客无 */ + private Long senderId; + + /** 回复用户 ID,系统用户回复有值,游客无 */ + private Long receiverId; + + /** 发送用户昵称,游客回复有值,登录用户无 */ + private String senderNick; + + /** 回复用户昵称,游客回复有值,系统用户无 */ + private String receiverNick; + + /** 回复数据 */ + private String content; + + /** 发送用户 IP */ + private String ip; + + /** 被回复用户忽略该回复的时间 */ + private Long ignoredAt; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/entity/EmailQueue.java b/src/main/java/com/imyeyu/server/modules/common/entity/EmailQueue.java new file mode 100644 index 0000000..d2e32c1 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/entity/EmailQueue.java @@ -0,0 +1,43 @@ +package com.imyeyu.server.modules.common.entity; + +import com.imyeyu.spring.annotation.table.AutoUUID; +import com.imyeyu.spring.annotation.table.Id; +import lombok.Data; + +/** + * 邮件队列 + * + * @author 夜雨 + * @since 2021-08-24 14:59 + */ +@Data +public class EmailQueue { + + /** + * 业务类型 + * + * @author 夜雨 + * @since 2021-08-24 15:54 + */ + public enum BizType { + + /** 回复提醒 */ + REPLY_REMINAD, + + /** 邮箱验证 */ + EMAIL_VERIFY, + + /** 重置密码 */ + RESET_PASSWORD + } + + @Id + @AutoUUID + private String UUID; + + private BizType bizType; + + private Long bizId; + + private Long sendAt; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/entity/EmailQueueLog.java b/src/main/java/com/imyeyu/server/modules/common/entity/EmailQueueLog.java new file mode 100644 index 0000000..21a82ac --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/entity/EmailQueueLog.java @@ -0,0 +1,23 @@ +package com.imyeyu.server.modules.common.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.entity.Entity; + +/** + * @author 夜雨 + * @since 2021-08-24 18:00 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class EmailQueueLog extends Entity { + + private String UUID; + private EmailQueue.BizType bizType; + private Long bizId; + private String sendTo; + private Long sendAt; + + private Boolean isSent; + private String exceptionMsg; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/entity/Feedback.java b/src/main/java/com/imyeyu/server/modules/common/entity/Feedback.java new file mode 100644 index 0000000..b7868f6 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/entity/Feedback.java @@ -0,0 +1,21 @@ +package com.imyeyu.server.modules.common.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.entity.Entity; + +/** + * 反馈 + * + * @author 夜雨 + * @since 2021-11-16 22:22 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class Feedback extends Entity { + + private String from; + private String email; + private String data; + private String ip; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/entity/Icon.java b/src/main/java/com/imyeyu/server/modules/common/entity/Icon.java new file mode 100644 index 0000000..152c9a3 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/entity/Icon.java @@ -0,0 +1,25 @@ +package com.imyeyu.server.modules.common.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.entity.Entity; + +/** + * 字体图标 + * + * @author 夜雨 + * @since 2022-09-09 10:54 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class Icon extends Entity { + + /** 名称 */ + private String name; + + /** Unicode */ + private String unicode; + + /** SVG 路径 */ + private String svg; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/entity/Multilingual.java b/src/main/java/com/imyeyu/server/modules/common/entity/Multilingual.java new file mode 100644 index 0000000..29dc150 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/entity/Multilingual.java @@ -0,0 +1,67 @@ +package com.imyeyu.server.modules.common.entity; + +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.java.ref.Ref; +import com.imyeyu.server.TimiServerAPI; +import com.imyeyu.spring.entity.Entity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.lang.reflect.Field; + +/** + * @author 夜雨 + * @since 2023-10-24 16:41 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class Multilingual extends Entity { + + protected String key; + + protected String zhCN; + + protected String zhTW; + + protected String enUS; + + protected String ruRU; + + protected String koKR; + + protected String jaJP; + + protected String deDE; + + /** @return 根据用户环境获取语言值 */ + public String getValue() { + try { + Field field = Ref.getField(getClass(), TimiServerAPI.getUserLanguage().toString().replace("_", "")); + if (field == null) { + throw new TimiException(TimiCode.RESULT_NULL).msgKey("TODO not support language"); + } + return Ref.getFieldValue(this, field, String.class); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + /** + * 获取指定语言值 + * + * @param language 指定语言 + * @return 值 + */ + public String getValue(com.imyeyu.java.bean.Language language) { + try { + Field field = Ref.getField(getClass(), language.toString().replace("_", "")); + if (field == null) { + throw new TimiException(TimiCode.RESULT_NULL).msgKey("TODO not support language"); + } + return Ref.getFieldValue(this, field, String.class); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/entity/Setting.java b/src/main/java/com/imyeyu/server/modules/common/entity/Setting.java new file mode 100644 index 0000000..f69e32a --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/entity/Setting.java @@ -0,0 +1,51 @@ +package com.imyeyu.server.modules.common.entity; + +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.spring.annotation.table.Id; +import com.imyeyu.spring.entity.Creatable; +import com.imyeyu.spring.entity.Updatable; +import lombok.Data; + +/** + * 系统配置 + * + * @author 夜雨 + * @since 2021-07-20 21:46 + */ +@Data +public class Setting implements Creatable, Updatable { + + /** + * + * + * @author 夜雨 + * @since 2025-01-10 17:08 + */ + public enum Type { + + INTEGER, + + STRING, + + JSON, + + YAML, + } + + @Id + private SettingKey key; + + private String value; + + private Type type; + + private boolean isPrivate; + + private Long createdAt; + + private Long updatedAt; + + public boolean isPublic() { + return !isPrivate; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/entity/Tag.java b/src/main/java/com/imyeyu/server/modules/common/entity/Tag.java new file mode 100644 index 0000000..f08c751 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/entity/Tag.java @@ -0,0 +1,39 @@ +package com.imyeyu.server.modules.common.entity; + +import com.imyeyu.server.bean.MultilingualHandler; +import com.imyeyu.spring.entity.Entity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * @author 夜雨 + * @since 2024-08-28 14:26 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class Tag extends Entity implements MultilingualHandler { + + /** + * + * + * @author 夜雨 + * @since 2024-08-28 14:26 + */ + public enum BizType { + + ARTICLE, + + MUSIC, + + SERVER_FILE, + + WALLPAPER + } + + protected BizType bizType; + + protected String bizID; + + @MultilingualHandler.MultilingualField + protected String value; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/entity/Task.java b/src/main/java/com/imyeyu/server/modules/common/entity/Task.java new file mode 100644 index 0000000..d97c09c --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/entity/Task.java @@ -0,0 +1,40 @@ +package com.imyeyu.server.modules.common.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import com.imyeyu.spring.entity.Entity; + +import java.util.List; + +/** + * 开发任务 + * + * @author 夜雨 + * @since 2022-02-26 11:12 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class Task extends Entity { + + /** + * 任务状态 + * + * @author 夜雨 + * @since 2022-02-26 11:53 + */ + @Getter + public enum Status { + + UPDATE, WITH, WAIT, KEEP, DIE; + + final int sort = ordinal(); + } + + private String name; + private Status status; + private String digest; + + // 关联数据 + private List details; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/entity/TaskDetail.java b/src/main/java/com/imyeyu/server/modules/common/entity/TaskDetail.java new file mode 100644 index 0000000..0d046e2 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/entity/TaskDetail.java @@ -0,0 +1,48 @@ +package com.imyeyu.server.modules.common.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import com.imyeyu.spring.entity.Entity; + +/** + * 开发任务详细信息 + * + * @author 夜雨 + * @since 2022-02-27 17:58 + */ +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class TaskDetail extends Entity { + + /** + * 类型 + * + * @author 夜雨 + * @since 2022-02-27 18:03 + */ + @Getter + public enum Type { + + BUG, FEATURE + } + + /** + * 状态 + * + * @author 夜雨 + * @since 2022-02-27 18:06 + */ + @Getter + public enum Status { + + UPDATE, WAIT, FINISH, CLOSE + } + + private Long taskId; + private Type type; + private Status status; + private String digest; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/entity/Template.java b/src/main/java/com/imyeyu/server/modules/common/entity/Template.java new file mode 100644 index 0000000..03a8224 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/entity/Template.java @@ -0,0 +1,33 @@ +package com.imyeyu.server.modules.common.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.entity.Entity; + +/** + * @author 夜雨 + * @since 2023-09-21 00:53 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class Template extends Entity { + + /** + * + * + * @author 夜雨 + * @since 2023-09-22 16:38 + */ + public enum BizType { + + GIT, + + FOREVER_MC + } + + private BizType bizType; + + private String bizCode; + + private String data; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/entity/User.java b/src/main/java/com/imyeyu/server/modules/common/entity/User.java new file mode 100644 index 0000000..06a92d3 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/entity/User.java @@ -0,0 +1,68 @@ +package com.imyeyu.server.modules.common.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.entity.Entity; +import com.imyeyu.utils.Time; + +/** + * 用户 + * + * @author 夜雨 + * @since 2021-03-01 17:11 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class User extends Entity { + + /** + * + * + * @author 夜雨 + * @since 2024-02-21 14:48 + */ + public enum AttachType { + + AVATAR, + + WRAPPER, + + LICENSE, + + DEFAULT_AVATAR, + + DEFAULT_WRAPPER + } + + /** 用户名 */ + protected String name; + + /** 密码 */ + protected String password; + + /** 邮箱 */ + protected String email; + + /** 邮箱验证时间 */ + protected Long emailVerifyAt; + + /** 解除禁言时间 */ + protected Long unmuteAt; + + /** 解除封禁时间 */ + protected Long unbanAt; + + /** @return true 为禁言中 */ + public boolean isMuting() { + return unmuteAt != null && Time.now() < unmuteAt; + } + + /** @return true 为封禁中 */ + public boolean isBanning() { + return unbanAt != null && Time.now() < unbanAt; + } + + public boolean emailVerified() { + return emailVerifyAt != null; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/entity/UserConfig.java b/src/main/java/com/imyeyu/server/modules/common/entity/UserConfig.java new file mode 100644 index 0000000..e52c37c --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/entity/UserConfig.java @@ -0,0 +1,33 @@ +package com.imyeyu.server.modules.common.entity; + +import com.imyeyu.spring.annotation.table.Id; +import com.imyeyu.spring.entity.Updatable; +import jakarta.validation.constraints.Min; +import lombok.Data; + +/** + * 用户设置 + * + * @author 夜雨 + * @since 2021-08-12 15:06 + */ +@Data +public class UserConfig implements Updatable { + + @Min(1) + @Id + private Long userId; + + private Boolean emailReplyRemind; + + private Long updatedAt; + + public UserConfig(Long uid) { + this.userId = uid; + emailReplyRemind = true; + } + + public Boolean isEmailReplyRemind() { + return emailReplyRemind; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/entity/UserPrivacy.java b/src/main/java/com/imyeyu/server/modules/common/entity/UserPrivacy.java new file mode 100644 index 0000000..c76cf9a --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/entity/UserPrivacy.java @@ -0,0 +1,51 @@ +package com.imyeyu.server.modules.common.entity; + +import com.imyeyu.java.ref.Ref; +import com.imyeyu.spring.annotation.table.Id; +import com.imyeyu.spring.entity.Updatable; +import jakarta.validation.constraints.Min; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.lang.reflect.Field; +import java.util.List; + +/** + * 用户隐私控制 + * + * @author 夜雨 + * @since 2021-07-27 16:51 + */ +@Data +@NoArgsConstructor +public class UserPrivacy implements Updatable { + + @Min(1) + @Id + private Long userId; + + private boolean email; + private boolean sex; + private boolean birthdate; + private boolean qq; + private boolean lastLoginAt; + private boolean createdAt; + + private Long updatedAt; + + public UserPrivacy(Long uid) { + this.userId = uid; + } + + /** @return 过滤字段列表 */ + public List listFilterFields() { + return Ref.listFields(getClass()).stream().filter(f -> { + try { + f.setAccessible(true); + return boolean.class.isAssignableFrom(f.getType()) && !(boolean) f.get(this); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }).map(Field::getName).toList(); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/entity/UserProfile.java b/src/main/java/com/imyeyu/server/modules/common/entity/UserProfile.java new file mode 100644 index 0000000..8d514b5 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/entity/UserProfile.java @@ -0,0 +1,66 @@ +package com.imyeyu.server.modules.common.entity; + +import com.imyeyu.server.modules.common.bean.ImageType; +import com.imyeyu.spring.annotation.table.Id; +import com.imyeyu.spring.entity.Updatable; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 用户数据 + * + * @author 夜雨 + * @since 2021-05-29 15:58 + */ +@Data +@NoArgsConstructor +public class UserProfile implements Updatable { + + /** 用户 ID */ + @Min(1) + @Id + protected Long userId; + + /** 封面类型 */ + protected ImageType wrapperType; + + /** 头像类型 */ + protected ImageType avatarType; + + /** 经验值 */ + protected Integer exp; + + /** 性别 */ + @Max(1) + @Min(0) + protected Byte sex; + + /** 出生日期 */ + @Min(0) + protected Long birthdate; + + /** QQ */ + @Pattern(regexp = "[1-9]\\d{4,14}") + protected String qq; + + /** 说明 */ + @Size(max = 240) + protected String description; + + /** 最近登录 IP */ + protected String lastLoginIP; + + /** 最近登录时间 */ + protected Long lastLoginAt; + + /** 修改时间 */ + protected Long updatedAt; + + public UserProfile(Long userId) { + this.userId = userId; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/entity/Version.java b/src/main/java/com/imyeyu/server/modules/common/entity/Version.java new file mode 100644 index 0000000..00734b1 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/entity/Version.java @@ -0,0 +1,21 @@ +package com.imyeyu.server.modules.common.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.entity.Entity; + +/** + * 版本管理 + * + * @author 夜雨 + * @since 2021-06-10 16:01 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class Version extends Entity { + + private String name; + private String version; + private String content; + private String url; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/mapper/AttachmentMapper.java b/src/main/java/com/imyeyu/server/modules/common/mapper/AttachmentMapper.java new file mode 100644 index 0000000..e4db268 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/mapper/AttachmentMapper.java @@ -0,0 +1,31 @@ +package com.imyeyu.server.modules.common.mapper; + +import com.imyeyu.server.modules.common.entity.Attachment; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2023-08-15 10:22 + */ +public interface AttachmentMapper extends BaseMapper { + + /** 有效条件,非删除和销毁 */ + String VALID = NOT_DELETE + " AND destroy_at IS NULL"; + + @Select("SELECT * FROM attachment WHERE biz_type = #{bizType} AND biz_id = #{bizId} " + VALID + LIMIT_1) + Attachment selectByBizId(Attachment.BizType bizType, long bizId); + + @Select("SELECT * FROM attachment WHERE mongo_id = #{mongoId} " + VALID + LIMIT_1) + Attachment selectByMongoId(String mongoId); + + @Select("SELECT * FROM attachment WHERE biz_type = #{bizType} AND biz_id = #{bizId} AND attach_type = #{attachType} " + VALID + LIMIT_1) + Attachment selectByAttachType(Attachment.BizType bizType, long bizId, Enum attachType); + + @Select("SELECT * FROM attachment WHERE biz_type = #{bizType} AND biz_id = #{bizId} AND " + VALID + PAGE) + List listByBizId(Attachment.BizType bizType, long bizId, long offset, int limit); + + List listByAttachType(Attachment.BizType bizType, long bizId, Enum ...attachTypes); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/mapper/CommentMapper.java b/src/main/java/com/imyeyu/server/modules/common/mapper/CommentMapper.java new file mode 100644 index 0000000..144a372 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/mapper/CommentMapper.java @@ -0,0 +1,40 @@ +package com.imyeyu.server.modules.common.mapper; + +import com.imyeyu.server.modules.common.entity.Comment; +import com.imyeyu.server.modules.common.vo.comment.CommentView; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.LinkedHashMap; +import java.util.List; + +/** + * 评论 + * + * @author 夜雨 + * @since 2021-2-23 21:33 + */ +public interface CommentMapper extends BaseMapper { + + @Select("SELECT * FROM comment WHERE id = #{id}" + NOT_DELETE) + Comment select(Long id); + + @Update("UPDATE comment SET deleted_at = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) WHERE id = #{id}") + @Override + void delete(Long id); + + @Select("SELECT COUNT(1) FROM comment WHERE biz_type = #{bizType} AND biz_id = #{bizId}" + NOT_DELETE) + long count(Comment.BizType bizType, Long bizId); + + long countAll(Comment.BizType bizType, Long bizId); + + List list(Comment.BizType bizType, Long bizId, Long offset, int limit, LinkedHashMap orderMap); + + long countByUserId(Long userId); + + List listByUserId(Long userId, Long offset, int limit, LinkedHashMap orderMap); + + @Update("UPDATE comment SET deleted_at = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) WHERE user_id = #{userId} ") + void deleteByUserId(Long userId); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/mapper/CommentRemindQueueMapper.java b/src/main/java/com/imyeyu/server/modules/common/mapper/CommentRemindQueueMapper.java new file mode 100644 index 0000000..3bc24b9 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/mapper/CommentRemindQueueMapper.java @@ -0,0 +1,26 @@ +package com.imyeyu.server.modules.common.mapper; + +import com.imyeyu.server.modules.blog.entity.CommentRemindQueue; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 评论回复提醒队列 + * + * @author 夜雨 + * @since 2021-08-25 00:15 + */ +public interface CommentRemindQueueMapper extends BaseMapper { + + @Select("SELECT * FROM comment_remind_queue WHERE user_id = #{userId}") + List listByUserId(Long userId); + + @Delete("DELETE FROM comment_remind_queue WHERE user_id = #{userId}") + void destroyByUserId(Long userId); + + @Delete("DELETE FROM comment_remind_queue WHERE reply_id = #{replyId}") + void destroyByReplyId(Long replyId); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/mapper/CommentReplyMapper.java b/src/main/java/com/imyeyu/server/modules/common/mapper/CommentReplyMapper.java new file mode 100644 index 0000000..9cce388 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/mapper/CommentReplyMapper.java @@ -0,0 +1,35 @@ +package com.imyeyu.server.modules.common.mapper; + + +import com.imyeyu.server.modules.common.entity.CommentReply; +import com.imyeyu.server.modules.common.vo.comment.CommentReplyPage; +import com.imyeyu.server.modules.common.vo.comment.CommentReplyView; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +/** + * 评论回复 + * + * @author 夜雨 + * @since 2021-08-24 10:36 + */ +public interface CommentReplyMapper extends BaseMapper { + + @Select("SELECT * FROM comment_reply WHERE sender_id = #{senderId}" + NOT_DELETE) + List listAllBySenderId(Long senderId); + + @Select("SELECT COUNT(1) FROM comment_reply WHERE ${bizType.column} = #{bizId}" + NOT_DELETE) + long countByBizType(CommentReplyPage.BizType bizType, Long bizId); + + @Select("SELECT * FROM comment_reply WHERE ${bizType.column} = #{bizId} AND ignored_at IS NULL" + NOT_DELETE + PAGE) + List listByBizType(CommentReplyPage.BizType bizType, Long bizId, Long offset, int limit); + + @Update("UPDATE comment_reply SET deleted_at = " + UNIX_TIME + " WHERE sender_id = #{userId} OR receiver_id = #{userId}") + void deleteByUserId(Long userId); + + @Update("UPDATE comment_reply SET deleted_at = " + UNIX_TIME + " WHERE comment_id = #{commentId}") + void deleteByCommentId(Long commentId); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/mapper/EmailQueueLogMapper.java b/src/main/java/com/imyeyu/server/modules/common/mapper/EmailQueueLogMapper.java new file mode 100644 index 0000000..dced490 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/mapper/EmailQueueLogMapper.java @@ -0,0 +1,11 @@ +package com.imyeyu.server.modules.common.mapper; + +import com.imyeyu.server.modules.common.entity.EmailQueueLog; +import com.imyeyu.spring.mapper.BaseMapper; + +/** + * @author 夜雨 + * @since 2023-08-10 10:38 + */ +public interface EmailQueueLogMapper extends BaseMapper { +} diff --git a/src/main/java/com/imyeyu/server/modules/common/mapper/EmailQueueMapper.java b/src/main/java/com/imyeyu/server/modules/common/mapper/EmailQueueMapper.java new file mode 100644 index 0000000..64c4b6e --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/mapper/EmailQueueMapper.java @@ -0,0 +1,22 @@ +package com.imyeyu.server.modules.common.mapper; + +import com.imyeyu.server.modules.common.entity.EmailQueue; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 邮件推送队列 + * + * @author 夜雨 + * @since 2021-08-24 16:22 + */ +public interface EmailQueueMapper extends BaseMapper { + + @Select("SELECT * FROM email_queue WHERE biz_type = #{bizType} AND biz_id = #{bizId}") + EmailQueue query(EmailQueue.BizType bizType, Long bizId); + + @Select("SELECT * FROM email_queue") + List listAll(); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/mapper/FeedbackMapper.java b/src/main/java/com/imyeyu/server/modules/common/mapper/FeedbackMapper.java new file mode 100644 index 0000000..f2de4fd --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/mapper/FeedbackMapper.java @@ -0,0 +1,13 @@ +package com.imyeyu.server.modules.common.mapper; + +import com.imyeyu.server.modules.common.entity.Feedback; +import com.imyeyu.spring.mapper.BaseMapper; + +/** + * 反馈 + * + * @author 夜雨 + * @since 2021-11-16 22:28 + */ +public interface FeedbackMapper extends BaseMapper { +} diff --git a/src/main/java/com/imyeyu/server/modules/common/mapper/IconMapper.java b/src/main/java/com/imyeyu/server/modules/common/mapper/IconMapper.java new file mode 100644 index 0000000..b78d5ff --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/mapper/IconMapper.java @@ -0,0 +1,43 @@ +package com.imyeyu.server.modules.common.mapper; + +import com.imyeyu.server.modules.common.entity.Icon; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 图标 + * + * @author 夜雨 + * @since 2022-09-15 00:02 + */ +public interface IconMapper extends BaseMapper { + + @Select("SELECT COUNT(1) FROM icon" + NOT_DELETE) + @Override + long count(); + + @Select("SELECT * FROM icon LIMIT #{offset}, #{limit}" + NOT_DELETE) + @Override + List list(long offset, int limit); + + @Select("SELECT * FROM icon WHERE 1 = 1" + NOT_DELETE) + List listAll(); + + @Select("SELECT COUNT(1) FROM icon WHERE name LIKE CONCAT('%', #{name}, '%')" + NOT_DELETE) + long countByName(String name); + + @Select("SELECT * FROM icon WHERE name LIKE CONCAT('%', #{name}, '%')" + PAGE) + List listByName(String name, long offset, int limit); + + long countByLabel(String lang, String label); + + List listByLabel(String lang, String label, long offset, int limit); + + @Select("SELECT COUNT(1) FROM icon WHERE unicode = #{unicode}" + NOT_DELETE) + long countByUnicode(String unicode); + + @Select("SELECT * FROM icon WHERE unicode = #{unicode}" + PAGE) + List listByUnicode(String unicode, long offset, int limit); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/mapper/MultilingualMapper.java b/src/main/java/com/imyeyu/server/modules/common/mapper/MultilingualMapper.java new file mode 100644 index 0000000..c05b28d --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/mapper/MultilingualMapper.java @@ -0,0 +1,32 @@ +package com.imyeyu.server.modules.common.mapper; + +import com.imyeyu.server.modules.common.entity.Multilingual; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2023-10-25 10:47 + */ +public interface MultilingualMapper extends BaseMapper { + + // 以下临时 + + @Select("SELECT * FROM multilingual WHERE key LIKE CONCAT('%', #{key}, '%')" + NOT_DELETE) + List selectByKeyLike(String key); + + List selectByKeyList(List keys); + + // 以上临时 + + @Select("SELECT * FROM multilingual WHERE zh_cn = #{zhCN}" + NOT_DELETE + LIMIT_1) + Multilingual selectByZhCN(String zhCN); + + @Select("SELECT * FROM multilingual WHERE en_US IS NULL OR en_US = ''" + NOT_DELETE) + List selectByNotTranslate(); + + @Select("SELECT * FROM multilingual WHERE `key` = #{key}" + NOT_DELETE + LIMIT_1) + Multilingual selectByKey(String key); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/mapper/SettingMapper.java b/src/main/java/com/imyeyu/server/modules/common/mapper/SettingMapper.java new file mode 100644 index 0000000..5f41a47 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/mapper/SettingMapper.java @@ -0,0 +1,23 @@ +package com.imyeyu.server.modules.common.mapper; + +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.entity.Setting; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 系统配置 + * + * @author 夜雨 + * @since 2021-07-20 22:26 + */ +public interface SettingMapper extends BaseMapper { + + @Select("SELECT * FROM `setting` WHERE `key` = #{key}") + Setting selectByKey(SettingKey key); + + @Select("SELECT * FROM `setting`") + List listAll(); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/mapper/TagMapper.java b/src/main/java/com/imyeyu/server/modules/common/mapper/TagMapper.java new file mode 100644 index 0000000..de7afd0 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/mapper/TagMapper.java @@ -0,0 +1,11 @@ +package com.imyeyu.server.modules.common.mapper; + +import com.imyeyu.server.modules.common.entity.Tag; +import com.imyeyu.spring.mapper.BaseMapper; + +/** + * @author 夜雨 + * @since 2025-05-30 22:48 + */ +public interface TagMapper extends BaseMapper { +} diff --git a/src/main/java/com/imyeyu/server/modules/common/mapper/TaskMapper.java b/src/main/java/com/imyeyu/server/modules/common/mapper/TaskMapper.java new file mode 100644 index 0000000..dafa493 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/mapper/TaskMapper.java @@ -0,0 +1,16 @@ +package com.imyeyu.server.modules.common.mapper; + +import com.imyeyu.server.modules.common.entity.Task; + +import java.util.List; + +/** + * 任务服务 + * + * @author 夜雨 + * @since 2022-04-03 15:37 + */ +public interface TaskMapper { + + List listAll4Public(); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/mapper/TemplateMapper.java b/src/main/java/com/imyeyu/server/modules/common/mapper/TemplateMapper.java new file mode 100644 index 0000000..899b10a --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/mapper/TemplateMapper.java @@ -0,0 +1,15 @@ +package com.imyeyu.server.modules.common.mapper; + +import com.imyeyu.server.modules.common.entity.Template; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; + +/** + * @author 夜雨 + * @since 2023-09-22 16:41 + */ +public interface TemplateMapper extends BaseMapper { + + @Select("SELECT * FROM template WHERE biz_type = #{bizType} AND biz_code = #{bizCode}" + NOT_DELETE) + Template query(Template.BizType bizType, String bizCode); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/mapper/UserConfigMapper.java b/src/main/java/com/imyeyu/server/modules/common/mapper/UserConfigMapper.java new file mode 100644 index 0000000..5b656c4 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/mapper/UserConfigMapper.java @@ -0,0 +1,13 @@ +package com.imyeyu.server.modules.common.mapper; + +import com.imyeyu.server.modules.common.entity.UserConfig; +import com.imyeyu.spring.mapper.BaseMapper; + +/** + * 用户设置 + * + * @author 夜雨 + * @since 2021-08-12 16:36 + */ +public interface UserConfigMapper extends BaseMapper { +} diff --git a/src/main/java/com/imyeyu/server/modules/common/mapper/UserMapper.java b/src/main/java/com/imyeyu/server/modules/common/mapper/UserMapper.java new file mode 100644 index 0000000..82183d8 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/mapper/UserMapper.java @@ -0,0 +1,20 @@ +package com.imyeyu.server.modules.common.mapper; + +import com.imyeyu.server.modules.common.entity.User; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; + +/** + * 用户 + * + * @author 夜雨 + * @since 2021-02-23 21:33 + */ +public interface UserMapper extends BaseMapper { + + @Select("SELECT * FROM user WHERE BINARY name = #{name}" + NOT_DELETE + LIMIT_1) + User selectByName(String name); + + @Select("SELECT * FROM user WHERE BINARY email = #{email} AND email_verify_at IS NOT NULL" + NOT_DELETE + LIMIT_1) + User selectByEmail(String email); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/mapper/UserPrivacyMapper.java b/src/main/java/com/imyeyu/server/modules/common/mapper/UserPrivacyMapper.java new file mode 100644 index 0000000..ff5dece --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/mapper/UserPrivacyMapper.java @@ -0,0 +1,13 @@ +package com.imyeyu.server.modules.common.mapper; + +import com.imyeyu.server.modules.common.entity.UserPrivacy; +import com.imyeyu.spring.mapper.BaseMapper; + +/** + * 用户隐私控制 + * + * @author 夜雨 + * @since 2021-07-27 17:18 + */ +public interface UserPrivacyMapper extends BaseMapper { +} diff --git a/src/main/java/com/imyeyu/server/modules/common/mapper/UserProfileMapper.java b/src/main/java/com/imyeyu/server/modules/common/mapper/UserProfileMapper.java new file mode 100644 index 0000000..a2655c9 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/mapper/UserProfileMapper.java @@ -0,0 +1,13 @@ +package com.imyeyu.server.modules.common.mapper; + +import com.imyeyu.server.modules.common.entity.UserProfile; +import com.imyeyu.spring.mapper.BaseMapper; + +/** + * 用户数据 + * + * @author 夜雨 + * @since 2021-07-27 17:04 + */ +public interface UserProfileMapper extends BaseMapper { +} diff --git a/src/main/java/com/imyeyu/server/modules/common/mapper/VersionMapper.java b/src/main/java/com/imyeyu/server/modules/common/mapper/VersionMapper.java new file mode 100644 index 0000000..ed3519c --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/mapper/VersionMapper.java @@ -0,0 +1,17 @@ +package com.imyeyu.server.modules.common.mapper; + +import com.imyeyu.server.modules.common.entity.Version; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; + +/** + * 版本管理 + * + * @author 夜雨 + * @since 2021-06-10 16:08 + */ +public interface VersionMapper extends BaseMapper { + + @Select("SELECT * FROM version WHERE name = #{name}" + NOT_DELETE) + Version queryByName(String name); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/AttachmentService.java b/src/main/java/com/imyeyu/server/modules/common/service/AttachmentService.java new file mode 100644 index 0000000..5ad23e5 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/AttachmentService.java @@ -0,0 +1,56 @@ +package com.imyeyu.server.modules.common.service; + +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.common.entity.Attachment; +import com.imyeyu.server.modules.common.vo.attachment.AttachmentRequest; +import com.imyeyu.server.modules.common.vo.attachment.AttachmentView; +import com.imyeyu.spring.service.DeletableService; +import com.imyeyu.spring.service.DestroyableService; +import com.imyeyu.spring.service.GettableService; +import com.mongodb.client.gridfs.model.GridFSFile; + +import java.io.InputStream; +import java.util.List; + +/** + * 附件服务 + * + *

删除和销毁都为数据库软删除,但删除不删除 MongoDB 文件,而销毁则删除 MongoDB 文件 + * + * @author 夜雨 + * @since 2023-08-15 10:21 + */ +public interface AttachmentService extends GettableService, DeletableService, DestroyableService { + + /** + * + * + * @param request + */ + void create(AttachmentRequest request); + + Attachment getByBizId(Attachment.BizType bizType, long bizId); + + Attachment getByAttachType(Attachment.BizType bizType, long bizId, Enum attachType); + + Attachment getByMongoId(String mongoId); + + AttachmentView viewByMongoId(String mongoId); + + GridFSFile readByMongoId(String mongoId); + + InputStream getInputStreamByMongoId(String mongoId); + + byte[] readAllByMongoId(String mongoId); + + /** + * 根据业务获取所有附件 + * + * @param bizType 业务类型 + * @param bizId 业务 ID + * @param attachTypes + * @return 所有附件 + * @throws TimiException 服务异常 + */ + List listByBizId(Attachment.BizType bizType, long bizId, Enum ...attachTypes); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/CommentReplyService.java b/src/main/java/com/imyeyu/server/modules/common/service/CommentReplyService.java new file mode 100644 index 0000000..ae22f99 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/CommentReplyService.java @@ -0,0 +1,21 @@ +package com.imyeyu.server.modules.common.service; + +import com.imyeyu.server.modules.common.entity.CommentReply; +import com.imyeyu.server.modules.common.vo.comment.CommentReplyPage; +import com.imyeyu.server.modules.common.vo.comment.CommentReplyView; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.CreatableService; +import com.imyeyu.spring.service.DeletableService; +import com.imyeyu.spring.service.GettableService; +import com.imyeyu.spring.service.UpdatableService; + +/** + * 评论回复服务 + * + * @author 夜雨 + * @since 2021-08-24 10:33 + */ +public interface CommentReplyService extends CreatableService, GettableService, UpdatableService, DeletableService { + + PageResult pageByBizType(CommentReplyPage page); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/CommentService.java b/src/main/java/com/imyeyu/server/modules/common/service/CommentService.java new file mode 100644 index 0000000..fee0128 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/CommentService.java @@ -0,0 +1,31 @@ +package com.imyeyu.server.modules.common.service; + +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.common.entity.Comment; +import com.imyeyu.server.modules.common.vo.comment.CommentView; +import com.imyeyu.server.modules.common.vo.comment.UserCommentPage; +import com.imyeyu.server.modules.git.vo.issue.CommentPage; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.CreatableService; +import com.imyeyu.spring.service.DeletableService; +import com.imyeyu.spring.service.GettableService; + +/** + * 评论服务 + * + * @author 夜雨 + * @since 2021-02-23 21:32 + */ +public interface CommentService extends CreatableService, GettableService, DeletableService { + + PageResult pageByBizId(CommentPage page); + + /** + * 获取用户评论页面 + * + * @param userCommentPage 页面参数 + * @return 页面列表 + * @throws TimiException 服务异常 + */ + PageResult pageByUserId(UserCommentPage userCommentPage); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/EmailQueueService.java b/src/main/java/com/imyeyu/server/modules/common/service/EmailQueueService.java new file mode 100644 index 0000000..7c3d48d --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/EmailQueueService.java @@ -0,0 +1,52 @@ +package com.imyeyu.server.modules.common.service; + +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.common.entity.EmailQueue; +import com.imyeyu.server.modules.common.entity.EmailQueueLog; +import com.imyeyu.spring.service.CreatableService; +import com.imyeyu.spring.service.DestroyableService; + +import java.util.List; + +/** + * 邮件推送队列服务 + * + * @author 夜雨 + * @since 2021-08-24 16:20 + */ +public interface EmailQueueService extends CreatableService, DestroyableService { + + /** + * 根据推送类型和数据 ID 获取推送对象 + * + * @param type 推送类型 + * @param dataId 数据 ID + * @return 推送对象 + * @throws TimiException 服务异常 + */ + EmailQueue get(EmailQueue.BizType type, Long dataId); + + /** + * 移出队列 + * + * @param UUID UUID + * @throws TimiException 服务异常 + */ + void destroy(String UUID); + + /** + * 添加推送日志 + * + * @param log 推送日志 + * @throws TimiException 服务异常 + */ + void addLog(EmailQueueLog log); + + /** + * 遍历推送队列 + * + * @return 推送队列 + * @throws TimiException 服务异常 + */ + List listAll(); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/FeedbackService.java b/src/main/java/com/imyeyu/server/modules/common/service/FeedbackService.java new file mode 100644 index 0000000..5300346 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/FeedbackService.java @@ -0,0 +1,14 @@ +package com.imyeyu.server.modules.common.service; + +import com.imyeyu.server.modules.common.vo.FeedbackRequest; + +/** + * 反馈服务 + * + * @author 夜雨 + * @since 2021-11-16 22:27 + */ +public interface FeedbackService { + + void create(FeedbackRequest request); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/IconService.java b/src/main/java/com/imyeyu/server/modules/common/service/IconService.java new file mode 100644 index 0000000..c22992d --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/IconService.java @@ -0,0 +1,23 @@ +package com.imyeyu.server.modules.common.service; + +import com.imyeyu.server.modules.common.entity.Icon; +import com.imyeyu.server.modules.common.vo.icon.AllResponse; +import com.imyeyu.server.modules.common.vo.icon.NamePage; +import com.imyeyu.server.modules.common.vo.icon.UnicodePage; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.PageableService; + +/** + * 图标服务 + * + * @author 夜雨 + * @since 2022-09-09 16:47 + */ +public interface IconService extends PageableService { + + AllResponse listAll(Long latest); + + PageResult pageByName(NamePage page); + + PageResult pageByUnicode(UnicodePage page); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/MultilingualService.java b/src/main/java/com/imyeyu/server/modules/common/service/MultilingualService.java new file mode 100644 index 0000000..a20ff46 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/MultilingualService.java @@ -0,0 +1,24 @@ +package com.imyeyu.server.modules.common.service; + +import com.imyeyu.java.bean.Language; +import com.imyeyu.server.modules.common.entity.Multilingual; +import com.imyeyu.spring.service.UpdatableService; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2023-10-25 10:47 + */ +public interface MultilingualService extends UpdatableService { + + Long create(String key, String zhCN); + + Long createIfNotExist(String key, String zhCN); + + String get(Language language, Long id); + + String getByKey(Language language, String key); + + List listByNotTranslate(); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/SettingService.java b/src/main/java/com/imyeyu/server/modules/common/service/SettingService.java new file mode 100644 index 0000000..4654a94 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/SettingService.java @@ -0,0 +1,78 @@ +package com.imyeyu.server.modules.common.service; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.entity.Setting; +import com.imyeyu.spring.service.UpdatableService; + +import java.util.Arrays; +import java.util.List; + +/** + * 系统配置服务 + * + * @author 夜雨 + * @since 2021-07-20 22:06 + */ +public interface SettingService extends UpdatableService { + + default List listByKeys(SettingKey... keys) { + return listByKeys(Arrays.asList(keys)); + } + + List listByKeys(List keyList); + + Setting getByKey(SettingKey key); + + /** + * 获取指定类型配置值字符串 + * + * @param key 键 + * @return 配置值 + */ + String getAsString(SettingKey key); + + int getAsInt(SettingKey key); + + long getAsLong(SettingKey key); + + double getAsDouble(SettingKey key); + + /** + * 获取为布尔值 + * + * @param key 键 + * @return 配置值 + * @throws TimiException 服务异常 + */ + boolean is(SettingKey key); + + /** + * 获取为布尔值,并取反 + * + * @param key 键 + * @return 配置值 + * @throws TimiException 服务异常 + */ + boolean not(SettingKey key); + + JsonElement getAsJsonElement(SettingKey key); + + JsonObject getAsJsonObject(SettingKey key); + + JsonArray getAsJsonArray(SettingKey key); + + T fromJson(SettingKey key, Class clazz); + + T fromJson(SettingKey key, TypeToken typeToken); + + T fromYaml(SettingKey key, Class clazz); + + List listAll(); + + void flushCache(); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/TagService.java b/src/main/java/com/imyeyu/server/modules/common/service/TagService.java new file mode 100644 index 0000000..73f45b4 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/TagService.java @@ -0,0 +1,17 @@ +package com.imyeyu.server.modules.common.service; + +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.common.entity.Tag; +import com.imyeyu.spring.service.CreatableService; +import com.imyeyu.spring.service.DeletableService; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2025-05-30 22:46 + */ +public interface TagService extends CreatableService, DeletableService { + + List listByBizID(Tag.BizType bizType, String bizID) throws TimiException; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/TaskService.java b/src/main/java/com/imyeyu/server/modules/common/service/TaskService.java new file mode 100644 index 0000000..5bfae13 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/TaskService.java @@ -0,0 +1,23 @@ +package com.imyeyu.server.modules.common.service; + +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.common.entity.Task; + +import java.util.List; + +/** + * 任务服务 + * + * @author 夜雨 + * @since 2022-04-03 15:36 + */ +public interface TaskService { + + /** + * 查询所有公开任务 + * + * @return 公开任务列表 + * @throws TimiException 服务异常 + */ + List listAll4Public(); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/TemplateService.java b/src/main/java/com/imyeyu/server/modules/common/service/TemplateService.java new file mode 100644 index 0000000..ee60eb8 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/TemplateService.java @@ -0,0 +1,12 @@ +package com.imyeyu.server.modules.common.service; + +import com.imyeyu.server.modules.common.entity.Template; + +/** + * @author 夜雨 + * @since 2023-09-22 16:38 + */ +public interface TemplateService { + + Template get(Template.BizType bizType, String bizCode); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/UserConfigService.java b/src/main/java/com/imyeyu/server/modules/common/service/UserConfigService.java new file mode 100644 index 0000000..55e53e6 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/UserConfigService.java @@ -0,0 +1,15 @@ +package com.imyeyu.server.modules.common.service; + +import com.imyeyu.server.modules.common.entity.UserConfig; +import com.imyeyu.spring.service.CreatableService; +import com.imyeyu.spring.service.GettableService; +import com.imyeyu.spring.service.UpdatableService; + +/** + * 用户设置服务 + * + * @author 夜雨 + * @since 2021-08-12 16:23 + */ +public interface UserConfigService extends GettableService, CreatableService, UpdatableService { +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/UserPrivacyService.java b/src/main/java/com/imyeyu/server/modules/common/service/UserPrivacyService.java new file mode 100644 index 0000000..71a5dd5 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/UserPrivacyService.java @@ -0,0 +1,15 @@ +package com.imyeyu.server.modules.common.service; + +import com.imyeyu.server.modules.common.entity.UserPrivacy; +import com.imyeyu.spring.service.CreatableService; +import com.imyeyu.spring.service.GettableService; +import com.imyeyu.spring.service.UpdatableService; + +/** + * 用户隐私控制服务 + * + * @author 夜雨 + * @since 2021-07-27 17:18 + */ +public interface UserPrivacyService extends GettableService, CreatableService, UpdatableService { +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/UserProfileService.java b/src/main/java/com/imyeyu/server/modules/common/service/UserProfileService.java new file mode 100644 index 0000000..5ee06ed --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/UserProfileService.java @@ -0,0 +1,18 @@ +package com.imyeyu.server.modules.common.service; + +import com.imyeyu.server.modules.common.entity.UserProfile; +import com.imyeyu.server.modules.common.vo.user.UserRequest; +import com.imyeyu.spring.service.CreatableService; +import com.imyeyu.spring.service.GettableService; +import com.imyeyu.spring.service.UpdatableService; + +/** + * 用户数据服务 + * + * @author 夜雨 + * @since 2021-07-27 17:05 + */ +public interface UserProfileService extends GettableService, CreatableService, UpdatableService { + + void update(UserRequest request); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/UserService.java b/src/main/java/com/imyeyu/server/modules/common/service/UserService.java new file mode 100644 index 0000000..f2cc3be --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/UserService.java @@ -0,0 +1,149 @@ +package com.imyeyu.server.modules.common.service; + +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.common.entity.User; +import com.imyeyu.server.modules.common.vo.user.LoginRequest; +import com.imyeyu.server.modules.common.vo.user.LoginResponse; +import com.imyeyu.server.modules.common.vo.user.RegisterRequest; +import com.imyeyu.server.modules.common.vo.user.UserView; +import com.imyeyu.spring.service.GettableService; + +/** + * 用户管理服务 + *

操作任何用户数据前应保证调用 find,以确保用户存在且没有注销(不需要判断,不存在时 find 会抛异常),如果不需要返回数据,可以调用 + * 一次 exist(),也不需要判断 + *

经过令牌验证的操作也不需要检验用户是否存在,不存在的用户无法登录也无法生成正确的令牌 + * + * @author 夜雨 + * @since 2021-02-23 21:32 + */ +public interface UserService extends GettableService { + + /** + * 注册用户,传入参 User.password 是明文 + * + * @param request 注册数据 + * @return 登录返回数据 + * @throws TimiException 服务异常 + */ + LoginResponse register(RegisterRequest request); + + /** + * 执行登录 + * + * @param request 登录数据 + * @return 登录返回数据 + * @throws TimiException 服务异常 + */ + LoginResponse login(LoginRequest request); + + /** + * 用户视图,未经过权限过滤和隐私过滤数据 + * + * @param userId + * @return + * @throws TimiException + */ + UserView view(Long userId); + + /** + * 密码验证 + * + * @param digestPassword 摘要密码 + * @param password 明文密码 + * @return true 为无效密码 + * @throws TimiException 服务异常 + */ + boolean isInvalidPassword(String digestPassword, String password); + + /** + * 令牌登录 + * + * @return 登录结果 + */ + LoginResponse login4Token(); + + /** 退出登录 */ + void logout(); + + /** + * 查找用户 + * + * @param user UID、邮箱和用户名 + * @return 用户 + * @throws TimiException 服务异常 + */ + User get(String user); + + boolean isLogged(); + + User getLoginUser(); + + /** + * 根据用户名查找 + * + * @param name 用户名 + * @return 账号数据 + * @throws TimiException 服务异常 + */ + User getByName(String name); + + /** + * 根据邮箱查找 + * + * @param email 邮箱 + * @return 账号数据 + * @throws TimiException 服务异常 + */ + User getByEmail(String email); + + /** + * 发送邮箱验证邮件 + * + * @throws TimiException 服务异常 + */ + void sendEmailVerify(); + + /** + * 邮箱验证回调 + * + * @param key 邮件密钥(非登录令牌) + * @throws TimiException 服务异常 + */ + void emailVerifyCallback(String key); + + /** + * 修改密码 + * + * @param oldValue 旧密码 + * @param newValue 新密码 + * @throws TimiException 服务异常 + */ + void updatePassword(String oldValue, String newValue); + + + /** + * 发送找回密码验证邮件 + * + * @param user UID、用户名或邮箱 + * @throws TimiException 服务异常 + */ + void sendPasswordForgetVerify(String user); + + /** + * 重置密码(需要密钥,由找回密码 sendPasswordForgetVerify 接口通过发送邮件分发) + * + * @param key 密钥 + * @param password 明文密码 + * @throws TimiException 服务异常 + */ + void resetPasswordByKey(String key, String password); + + /** + * 注销 + * + * @param password 密码校验 + * @throws TimiException 服务异常 + */ + void cancel(String password); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/VersionService.java b/src/main/java/com/imyeyu/server/modules/common/service/VersionService.java new file mode 100644 index 0000000..8163692 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/VersionService.java @@ -0,0 +1,14 @@ +package com.imyeyu.server.modules.common.service; + +import com.imyeyu.server.modules.common.entity.Version; + +/** + * 版本管理服务 + * + * @author 夜雨 + * @since 2021-06-10 16:06 + */ +public interface VersionService { + + Version getByName(String name); +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/implement/AttachmentServiceImplement.java b/src/main/java/com/imyeyu/server/modules/common/service/implement/AttachmentServiceImplement.java new file mode 100644 index 0000000..d21315a --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/implement/AttachmentServiceImplement.java @@ -0,0 +1,149 @@ +package com.imyeyu.server.modules.common.service.implement; + +import com.imyeyu.io.IO; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.config.dbsource.TimiServerDBConfig; +import com.imyeyu.server.modules.common.entity.Attachment; +import com.imyeyu.server.modules.common.mapper.AttachmentMapper; +import com.imyeyu.server.modules.common.service.AttachmentService; +import com.imyeyu.server.modules.common.vo.attachment.AttachmentRequest; +import com.imyeyu.server.modules.common.vo.attachment.AttachmentView; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.utils.Time; +import com.mongodb.client.gridfs.GridFSBucket; +import com.mongodb.client.gridfs.model.GridFSFile; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.gridfs.GridFsTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +/** + * @author 夜雨 + * @since 2023-08-15 10:21 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AttachmentServiceImplement extends AbstractEntityService implements AttachmentService { + + private final AttachmentMapper mapper; + + private final GridFSBucket gridFSBucket; + private final GridFsTemplate gridFsTemplate; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void destroy(Long id) { + try { + Attachment attachment = get(id); + if (!attachment.isDeleted()) { + delete(id); + } + mapper.destroy(attachment.getId()); + gridFsTemplate.delete(Query.query(Criteria.where("_id").is(attachment.getMongoId()))); + } catch (Exception e) { + log.error("delete mongo file error", e); + throw new TimiException(TimiCode.ERROR).msgKey("TODO delete mongo file error"); + } + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void create(AttachmentRequest request) { + TimiException.required(request.getBizType(), "not found request.bizType"); + TimiException.required(request.getBizId(), "not found request.bizId"); + TimiException.required(request.getName(), "not found request.name"); + String mongoId = null; + try { + InputStream is = request.getInputStream(); + TimiException.required(is, "not found request.inputStream"); + TimiException.requiredTrue(is.available() != 0, "empty request.inputStream"); + + StringBuilder mongoName = new StringBuilder(request.getBizType().toString()); + if (TimiJava.isNotEmpty(request.getAttachType())) { + mongoName.append("_").append(request.getAttachType().toUpperCase()).append("_"); + } + mongoName.append(request.getName()); + + mongoId = gridFsTemplate.store(is, mongoName.toString()).toString(); + Attachment attachment = new Attachment(); + BeanUtils.copyProperties(request, attachment); + attachment.setMongoId(mongoId); + attachment.setCreatedAt(Time.now()); + mapper.insert(attachment); + } catch (Exception e) { + if (mongoId != null) { + gridFsTemplate.delete(Query.query(Criteria.where("_id").is(mongoId))); + } + log.error("create attachment error", e); + throw new TimiException(TimiCode.ARG_BAD).msgKey("TODO read attachment input stream error"); + } + } + + @Override + public Attachment getByBizId(Attachment.BizType bizType, long bizId) { + return mapper.selectByBizId(bizType, bizId); + } + + @Override + public Attachment getByAttachType(Attachment.BizType bizType, long bizId, Enum attachType) { + return mapper.selectByAttachType(bizType, bizId, attachType); + } + + @Override + public Attachment getByMongoId(String mongoId) { + return mapper.selectByMongoId(mongoId); + } + + @Override + public AttachmentView viewByMongoId(String mongoId) { + Attachment attachment = getByMongoId(mongoId); + if (attachment == null) { + throw new TimiException(TimiCode.RESULT_NULL).msgKey("TODO not found attachment"); + } + AttachmentView view = new AttachmentView(); + BeanUtils.copyProperties(attachment, view); + return view; + } + + @Override + public GridFSFile readByMongoId(String mongoId) { + Attachment view = mapper.selectByMongoId(mongoId); + return gridFsTemplate.findOne(new Query(Criteria.where("_id").is(view.getMongoId()))); + } + + @Override + public InputStream getInputStreamByMongoId(String mongoId) { + return gridFSBucket.openDownloadStream(readByMongoId(mongoId).getObjectId()); + } + + @Override + public byte[] readAllByMongoId(String mongoId) { + try { + return IO.toBytes(getInputStreamByMongoId(mongoId)); + } catch (IOException e) { + throw new TimiException(TimiCode.ERROR, "TODO 读取失败"); + } + } + + @Override + public List listByBizId(Attachment.BizType bizType, long bizId, Enum... attachTypes) { + return mapper.listByAttachType(bizType, bizId, attachTypes); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/implement/CommentReplyServiceImplement.java b/src/main/java/com/imyeyu/server/modules/common/service/implement/CommentReplyServiceImplement.java new file mode 100644 index 0000000..d95413e --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/implement/CommentReplyServiceImplement.java @@ -0,0 +1,196 @@ +package com.imyeyu.server.modules.common.service.implement; + +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.TimiServerAPI; +import com.imyeyu.server.config.dbsource.TimiServerDBConfig; +import com.imyeyu.server.modules.blog.entity.Article; +import com.imyeyu.server.modules.blog.entity.CommentRemindQueue; +import com.imyeyu.server.modules.blog.service.ArticleService; +import com.imyeyu.server.modules.blog.service.CommentRemindQueueService; +import com.imyeyu.server.modules.common.bean.CommentSupport; +import com.imyeyu.server.modules.common.entity.Comment; +import com.imyeyu.server.modules.common.entity.CommentReply; +import com.imyeyu.server.modules.common.entity.EmailQueue; +import com.imyeyu.server.modules.common.entity.User; +import com.imyeyu.server.modules.common.entity.UserConfig; +import com.imyeyu.server.modules.common.mapper.CommentReplyMapper; +import com.imyeyu.server.modules.common.service.CommentReplyService; +import com.imyeyu.server.modules.common.service.CommentService; +import com.imyeyu.server.modules.common.service.EmailQueueService; +import com.imyeyu.server.modules.common.service.UserConfigService; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.server.modules.common.vo.comment.CommentReplyPage; +import com.imyeyu.server.modules.common.vo.comment.CommentReplyView; +import com.imyeyu.server.modules.common.vo.comment.CommentView; +import com.imyeyu.server.modules.git.service.RepositoryService; +import com.imyeyu.spring.TimiSpring; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.spring.service.GettableService; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.BeanUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.UUID; + +/** + * 评论回复服务实现 + * + * @author 夜雨 + * @since 2021-08-24 10:34 + */ +@Service +@RequiredArgsConstructor(onConstructor_ = {@Lazy}) +public class CommentReplyServiceImplement extends AbstractEntityService implements CommentReplyService { + + private final UserService userService; + private final CommentService commentService; + private final ArticleService articleService; + private final RepositoryService repositoryService; + private final UserConfigService userConfigService; + private final EmailQueueService emailQueueService; + private final CommentRemindQueueService commentRemindQueueService; + + private final CommentReplyMapper mapper; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void delete(Long crId) { + super.delete(crId); + commentRemindQueueService.destroyByReplyId(crId); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void create(CommentReply commentReply) { + Comment comment = commentService.get(commentReply.getCommentId()); + + Class> serviceClass = comment.getBizType().getServiceClass(); + GettableService service = TimiServerAPI.applicationContext.getBean(serviceClass); + CommentSupport commentSupport = service.get(comment.getBizId()); + if (commentSupport.canNotComment()) { + throw new TimiException(TimiCode.PERMISSION_ERROR).msgKey("评论已关闭"); + } + + CommentReply dbReply = new CommentReply(); + dbReply.setCommentId(comment.getId()); + + // 发送者 + String token = TimiSpring.getToken(); + if (TimiJava.isNotEmpty(token)) { + User senderUser = userService.getLoginUser(); + if (senderUser.isBanning() || senderUser.isMuting()) { + throw new TimiException(TimiCode.RESULT_BAN).msgKey("comment.banded"); + } + dbReply.setSenderId(senderUser.getId()); + dbReply.setSenderNick(null); + } else { + if (TimiJava.isEmpty(commentReply.getSenderNick())) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("comment.nick.empty"); + } + dbReply.setSenderNick(commentReply.getSenderNick()); + } + // 被回复 + dbReply.setReplyId(commentReply.getReplyId()); + if (commentReply.getReplyId() == null) { + // 回复主评论 + dbReply.setReceiverId(comment.getUserId()); + if (TimiJava.isEmpty(comment.getUserId())) { + dbReply.setReceiverNick(comment.getNick()); + } + } else { + // 回复其他回复 + CommentReply targetReply = get(commentReply.getReplyId()); + dbReply.setReceiverId(targetReply.getSenderId()); + if (TimiJava.isEmpty(targetReply.getSenderId())) { + dbReply.setReceiverNick(targetReply.getSenderNick()); + } + } + // 创建回复 + dbReply.setContent(commentReply.getContent()); + dbReply.setIp(TimiSpring.getRequestIP()); + super.create(dbReply); + // 被回复为注册用户时处理通知和邮件 + { + if (TimiJava.isEmpty(dbReply.getReceiverId())) { + return; + } + if (TimiJava.isNotEmpty(dbReply.getSenderId()) && dbReply.getSenderId().equals(dbReply.getReceiverId())) { + return; + } + // 被回复账号 + User receiverUser = userService.get(dbReply.getReceiverId()); + UserConfig userConfig = userConfigService.get(dbReply.getReceiverId()); + if (!receiverUser.emailVerified() || !userConfig.isEmailReplyRemind()) { + return; + } + // 添加提醒队列 + CommentRemindQueue remindQueue = new CommentRemindQueue(); + remindQueue.setUUID(UUID.randomUUID().toString()); + remindQueue.setUserId(receiverUser.getId()); + remindQueue.setReplyId(dbReply.getId()); + commentRemindQueueService.create(remindQueue); + + // 邮件队列 + EmailQueue emailQueue = emailQueueService.get(EmailQueue.BizType.REPLY_REMINAD, receiverUser.getId()); + if (emailQueue == null) { + emailQueue = new EmailQueue(); + emailQueue.setBizType(EmailQueue.BizType.REPLY_REMINAD); + emailQueue.setBizId(receiverUser.getId()); + long H10 = Time.H * 10; + if (Time.now() < Time.today() + H10) { + emailQueue.setSendAt(Time.today() + H10); + } else { + emailQueue.setSendAt(Time.tomorrow() + H10); + } + emailQueueService.create(emailQueue); + } + } + } + + @Override + public PageResult pageByBizType(CommentReplyPage page) { + PageResult result = new PageResult<>(); + List list = mapper.listByBizType(page.getBizType(), page.getBizId(), page.getOffset(), page.getLimit()); + for (int i = 0; i < list.size(); i++) { + CommentReplyView reply = list.get(i); + CommentView comment = new CommentView(); + BeanUtils.copyProperties(commentService.get(reply.getCommentId()), comment); + if (TimiJava.isNotEmpty(comment.getUserId())) { + comment.setUser(userService.view(comment.getUserId())); + } + switch (comment.getBizType()) { + case ARTICLE -> { + Article article = articleService.get(comment.getBizId()); + article.setData(null); + article.setExtendData(null); + comment.setArticle(article); + } +// case GIT_ISSUE, GIT_MERGE -> comment.setRepository(repositoryService.get(comment.getBizId())); + } + reply.setComment(comment); + + if (TimiJava.isNotEmpty(reply.getSenderId())) { + reply.setSender(userService.view(reply.getSenderId())); + } + if (TimiJava.isNotEmpty(reply.getReceiverId())) { + reply.setReceiver(userService.view(reply.getReceiverId())); + } + } + result.setList(list); + result.setTotal(mapper.countByBizType(page.getBizType(), page.getBizId())); + return result; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/implement/CommentServiceImplement.java b/src/main/java/com/imyeyu/server/modules/common/service/implement/CommentServiceImplement.java new file mode 100644 index 0000000..41ddb05 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/implement/CommentServiceImplement.java @@ -0,0 +1,159 @@ +package com.imyeyu.server.modules.common.service.implement; + +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.TimiServerAPI; +import com.imyeyu.server.config.dbsource.TimiServerDBConfig; +import com.imyeyu.server.modules.blog.entity.Article; +import com.imyeyu.server.modules.blog.service.ArticleService; +import com.imyeyu.server.modules.common.bean.CommentSupport; +import com.imyeyu.server.modules.common.entity.Comment; +import com.imyeyu.server.modules.common.entity.CommentReply; +import com.imyeyu.server.modules.common.entity.User; +import com.imyeyu.server.modules.common.mapper.CommentMapper; +import com.imyeyu.server.modules.common.mapper.CommentRemindQueueMapper; +import com.imyeyu.server.modules.common.mapper.CommentReplyMapper; +import com.imyeyu.server.modules.common.service.CommentService; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.server.modules.common.vo.comment.CommentReplyView; +import com.imyeyu.server.modules.common.vo.comment.CommentView; +import com.imyeyu.server.modules.common.vo.comment.UserCommentPage; +import com.imyeyu.server.modules.git.service.RepositoryService; +import com.imyeyu.server.modules.git.vo.issue.CommentPage; +import com.imyeyu.spring.TimiSpring; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.spring.service.GettableService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.LinkedHashMap; +import java.util.List; + +/** + * 评论操作服务实现 + * + * @author 夜雨 + * @since 2021-02-23 21:41 + */ +@Service +@RequiredArgsConstructor(onConstructor_ = {@Lazy}) +public class CommentServiceImplement extends AbstractEntityService implements CommentService { + + private final UserService userService; + private final ArticleService articleService; + private final RepositoryService repositoryService; + + private final CommentMapper mapper; + private final CommentReplyMapper replyMapper; + private final CommentRemindQueueMapper remindQueueMapper; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void create(Comment comment) { + Class> serviceClass = comment.getBizType().getServiceClass(); + GettableService service = TimiServerAPI.applicationContext.getBean(serviceClass); + CommentSupport commentSupport = service.get(comment.getBizId()); + TimiException.requiredTrue(commentSupport.canComment(), "comment.not.can.comment"); + + // 令牌和账号验证 + if (TimiJava.isNotEmpty(TimiSpring.getToken())) { + User commentUser = userService.getLoginUser(); + TimiException.requiredTrue(!commentUser.isBanning(), "comment.user.banned"); + TimiException.requiredTrue(!commentUser.isMuting(), "comment.user.muting"); + comment.setUserId(commentUser.getId()); + comment.setNick(null); + } else { + TimiException.required(comment.getNick(), "comment.nick.empty"); + } + // 内容 + TimiException.required(comment.getContent(), "comment.data.empty"); + comment.setIp(TimiSpring.getRequestIP()); + super.create(comment); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void delete(Long cId) { + User user = userService.getLoginUser(); + + Comment comment = get(cId); + if (!comment.getUserId().equals(user.getId())) { + throw new TimiException(TimiCode.PERMISSION_ERROR).msgKey("token.illegal"); + } + List replies = replyMapper.listAllBySenderId(user.getId()); + for (int i = 0; i < replies.size(); i++) { + // 移出被回复者的回复提醒队列 + remindQueueMapper.destroyByReplyId(replies.get(i).getId()); + } + replyMapper.deleteByCommentId(cId); + super.delete(cId); + } + + @Override + public PageResult pageByBizId(CommentPage page) { + if (page.getOrderMap() == null) { + page.setOrderMap(new LinkedHashMap<>()); + } + if (page.getOrderMap().isEmpty()) { + page.getOrderMap().put("createdAt", BaseMapper.OrderType.DESC); + } + List list = mapper.list(page.getBizType(), page.getBizId(), page.getOffset(), page.getLimit(), page.getOrderMap()); + for (int i = 0; i < list.size(); i++) { + CommentView comment = list.get(i); + if (TimiJava.isNotEmpty(comment.getUserId())) { + comment.setUser(userService.view(comment.getUserId())); + } + List replies = comment.getReplies(); + for (int j = 0; j < replies.size(); j++) { + CommentReplyView reply = replies.get(j); + if (TimiJava.isNotEmpty(reply.getSenderId())) { + reply.setSender(userService.view(reply.getSenderId())); + } + if (TimiJava.isNotEmpty(reply.getReceiverId())) { + reply.setReceiver(userService.view(reply.getReceiverId())); + } + } + } + PageResult result = new PageResult<>(); + result.setList(list); + result.setTotal(mapper.count(page.getBizType(), page.getBizId())); + return result; + } + + @Override + public PageResult pageByUserId(UserCommentPage page) { + if (page.getOrderMap() == null) { + page.setOrderMap(new LinkedHashMap<>()); + } + if (page.getOrderMap().isEmpty()) { + page.getOrderMap().put("createdAt", BaseMapper.OrderType.DESC); + } + PageResult result = new PageResult<>(); + result.setList(mapper.listByUserId(page.getUserId(), page.getOffset(), page.getLimit(), page.getOrderMap())); + result.setTotal(mapper.countByUserId(page.getUserId())); + + for (int i = 0; i < result.getList().size(); i++) { + CommentView view = result.getList().get(i); + switch (view.getBizType()) { + case ARTICLE -> { + Article article = articleService.get(view.getBizId()); + article.setData(null); + article.setExtendData(null); + view.setArticle(article); + } +// case GIT_ISSUE, GIT_MERGE -> view.setRepository(repositoryService.get(view.getBizId())); + } + } + return result; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/implement/EmailQueueServiceImplement.java b/src/main/java/com/imyeyu/server/modules/common/service/implement/EmailQueueServiceImplement.java new file mode 100644 index 0000000..55050e8 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/implement/EmailQueueServiceImplement.java @@ -0,0 +1,50 @@ +package com.imyeyu.server.modules.common.service.implement; + +import com.imyeyu.server.config.dbsource.TimiServerDBConfig; +import com.imyeyu.server.modules.common.entity.EmailQueue; +import com.imyeyu.server.modules.common.entity.EmailQueueLog; +import com.imyeyu.server.modules.common.mapper.EmailQueueLogMapper; +import com.imyeyu.server.modules.common.mapper.EmailQueueMapper; +import com.imyeyu.server.modules.common.service.EmailQueueService; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 邮件推送队列服务实现 + * + * @author 夜雨 + * @since 2021-08-24 16:21 + */ +@Service +@RequiredArgsConstructor +public class EmailQueueServiceImplement extends AbstractEntityService implements EmailQueueService { + + private final EmailQueueMapper mapper; + private final EmailQueueLogMapper logMapper; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public EmailQueue get(EmailQueue.BizType type, Long dataId) { + return mapper.query(type, dataId); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void addLog(EmailQueueLog log) { + logMapper.insert(log); + } + + @Override + public List listAll() { + return mapper.listAll(); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/implement/FeedbackServiceImplement.java b/src/main/java/com/imyeyu/server/modules/common/service/implement/FeedbackServiceImplement.java new file mode 100644 index 0000000..619f749 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/implement/FeedbackServiceImplement.java @@ -0,0 +1,40 @@ +package com.imyeyu.server.modules.common.service.implement; + +import com.imyeyu.server.config.dbsource.TimiServerDBConfig; +import com.imyeyu.server.modules.common.entity.Feedback; +import com.imyeyu.server.modules.common.mapper.FeedbackMapper; +import com.imyeyu.server.modules.common.service.FeedbackService; +import com.imyeyu.server.modules.common.vo.FeedbackRequest; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 反馈服务实现 + * + * @author 夜雨 + * @since 2021-11-16 22:27 + */ +@Service +@RequiredArgsConstructor +public class FeedbackServiceImplement extends AbstractEntityService implements FeedbackService { + + private final FeedbackMapper mapper; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void create(FeedbackRequest request) { + Feedback feedback = new Feedback(); + feedback.setFrom(request.getFrom()); + feedback.setEmail(request.getEmail()); + feedback.setData(request.getData()); + super.create(feedback); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/implement/IconServiceImplement.java b/src/main/java/com/imyeyu/server/modules/common/service/implement/IconServiceImplement.java new file mode 100644 index 0000000..13d05be --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/implement/IconServiceImplement.java @@ -0,0 +1,74 @@ +package com.imyeyu.server.modules.common.service.implement; + +import com.imyeyu.server.modules.common.entity.Icon; +import com.imyeyu.server.modules.common.mapper.IconMapper; +import com.imyeyu.server.modules.common.service.IconService; +import com.imyeyu.server.modules.common.vo.icon.AllResponse; +import com.imyeyu.server.modules.common.vo.icon.NamePage; +import com.imyeyu.server.modules.common.vo.icon.UnicodePage; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 图标服务实现 + * + * @author 夜雨 + * @since 2022-09-09 16:48 + */ +@Service +@RequiredArgsConstructor +public class IconServiceImplement extends AbstractEntityService implements IconService { + + private final IconMapper mapper; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public AllResponse listAll(Long latest) { + List list = mapper.listAll(); + long dbLatest = -1; + for (int i = 0; i < list.size(); i++) { + if (dbLatest < list.get(i).getCreatedAt()) { + dbLatest = list.get(i).getCreatedAt(); + } + if (list.get(i).getUpdatedAt() != null && dbLatest < list.get(i).getUpdatedAt()) { + dbLatest = list.get(i).getUpdatedAt(); + } + } + AllResponse response = new AllResponse(); + response.setLatest(dbLatest); + if (latest < dbLatest) { + // 存在更新 + response.setIcons(list); + } + return response; + } + + @Override + public PageResult pageByName(NamePage page) { + PageResult result = new PageResult<>(); + result.setList(mapper.listByName(page.getName(), page.getOffset(), page.getLimit())); + result.setTotal(mapper.countByName(page.getName())); + return result; + } + + @Override + public PageResult pageByUnicode(UnicodePage page) { + String unicode = page.getUnicode().toLowerCase(); + if (unicode.startsWith("0x") || unicode.startsWith("&#") || unicode.startsWith("\\u")) { + unicode = unicode.substring(2); + } + PageResult result = new PageResult<>(); + result.setList(mapper.listByUnicode(unicode, page.getOffset(), page.getLimit())); + result.setTotal(mapper.countByUnicode(unicode)); + return result; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/implement/MultilingualServiceImplement.java b/src/main/java/com/imyeyu/server/modules/common/service/implement/MultilingualServiceImplement.java new file mode 100644 index 0000000..d8f6d37 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/implement/MultilingualServiceImplement.java @@ -0,0 +1,95 @@ +package com.imyeyu.server.modules.common.service.implement; + +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.Language; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.entity.Multilingual; +import com.imyeyu.server.modules.common.mapper.MultilingualMapper; +import com.imyeyu.server.modules.common.service.MultilingualService; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.spring.util.Redis; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2023-10-25 10:48 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class MultilingualServiceImplement extends AbstractEntityService implements MultilingualService { + + private final SettingService settingService; + + private final MultilingualMapper mapper; + + private final Redis redisLanguageMap; + private final Redis redisLanguage; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public Long create(String key, String zhCN) { + Multilingual multilingual = new Multilingual(); + multilingual.setKey(key); + multilingual.setZhCN(zhCN); + mapper.insert(multilingual); + return multilingual.getId(); + } + + @Override + public Long createIfNotExist(String key, String zhCN) { + Multilingual existItem = mapper.selectByZhCN(zhCN); + if (TimiJava.isNotEmpty(existItem)) { + return existItem.getId(); + } + return create(key, zhCN); + } + + @Override + public String get(Language language, Long id) { + Multilingual result = redisLanguage.get(id); + if (result == null) { + result = mapper.select(id); + if (result == null) { + throw new TimiException(TimiCode.RESULT_NULL).msgKey("TODO not found language"); + } + redisLanguage.set(id, result, Time.D * settingService.getAsInt(SettingKey.TTL_MULTILINGUAL)); + } + return result.getValue(language); + } + + @Override + public String getByKey(Language language, String key) { + Long languageId = redisLanguageMap.get(key); + if (languageId == null) { + Multilingual result = mapper.selectByKey(key); + if (result == null) { + log.warn("not found language for key: {}", key); + return key; + } + long ttl = Time.D * settingService.getAsInt(SettingKey.TTL_MULTILINGUAL); + redisLanguage.set(result.getId(), result, ttl); + redisLanguageMap.set(result.getKey(), result.getId(), ttl); + return result.getValue(language); + } + return get(language, languageId); + } + + @Override + public List listByNotTranslate() { + return mapper.selectByNotTranslate(); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/implement/SettingServiceImplement.java b/src/main/java/com/imyeyu/server/modules/common/service/implement/SettingServiceImplement.java new file mode 100644 index 0000000..31b2a69 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/implement/SettingServiceImplement.java @@ -0,0 +1,151 @@ +package com.imyeyu.server.modules.common.service.implement; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.reflect.TypeToken; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.entity.Setting; +import com.imyeyu.server.modules.common.mapper.SettingMapper; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.spring.util.Redis; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.yaml.snakeyaml.Yaml; + +import java.util.ArrayList; +import java.util.List; + +/** + * 系统配置服务实现 + * + * @author 夜雨 + * @since 2021-07-20 22:11 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SettingServiceImplement extends AbstractEntityService implements SettingService { + + private final SettingMapper mapper; + private final Redis redisSetting; + + private final Gson gson; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + public Setting getByKey(SettingKey key) { + if (key == null) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("key can not be null"); + } + + String cacheValue = redisSetting.get(key.toString()); + if (TimiJava.isNotEmpty(cacheValue)) { + return gson.fromJson(cacheValue, Setting.class); + } + Setting setting = mapper.selectByKey(key); + if (TimiJava.isEmpty(setting.getValue())) { + return setting; + } + int settingTTL; + if (key == SettingKey.TTL_SETTING) { + settingTTL = Integer.parseInt(setting.getValue()); + } else { + settingTTL = Integer.parseInt(getByKey(SettingKey.TTL_SETTING).getValue()); + } + if (0 < settingTTL) { + redisSetting.set(key.toString(), gson.toJson(setting), Time.M * settingTTL); + } + return setting; + } + + @Override + public List listByKeys(List keyList) { + List result = new ArrayList<>(); + for (int i = 0; i < keyList.size(); i++) { + result.add(getByKey(keyList.get(i))); + } + return result; + } + + @Override + public String getAsString(SettingKey key) { + return getByKey(key).getValue(); + } + + @Override + public int getAsInt(SettingKey key) { + return Integer.parseInt(getAsString(key)); + } + + @Override + public long getAsLong(SettingKey key) { + return Long.parseLong(getAsString(key)); + } + + @Override + public double getAsDouble(SettingKey key) { + return Double.parseDouble(getAsString(key)); + } + + @Override + public boolean is(SettingKey key) { + return Boolean.parseBoolean(getAsString(key)); + } + + @Override + public boolean not(SettingKey key) { + return !is(key); + } + + @Override + public JsonElement getAsJsonElement(SettingKey key) { + return JsonParser.parseString(getAsString(key)); + } + + @Override + public JsonObject getAsJsonObject(SettingKey key) { + return getAsJsonElement(key).getAsJsonObject(); + } + + @Override + public JsonArray getAsJsonArray(SettingKey key) { + return getAsJsonElement(key).getAsJsonArray(); + } + + @Override + public T fromJson(SettingKey key, Class clazz) { + return gson.fromJson(getAsJsonElement(key), clazz); + } + + @Override + public T fromJson(SettingKey key, TypeToken typeToken) { + return gson.fromJson(getAsJsonElement(key), typeToken); + } + + public T fromYaml(SettingKey key, Class clazz) { + return new Yaml().loadAs(getAsString(key), clazz); + } + + @Override + public List listAll() { + return mapper.listAll(); + } + + @Override + public void flushCache() { + redisSetting.flushAll(); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/implement/TagServiceImplement.java b/src/main/java/com/imyeyu/server/modules/common/service/implement/TagServiceImplement.java new file mode 100644 index 0000000..fed010d --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/implement/TagServiceImplement.java @@ -0,0 +1,50 @@ +package com.imyeyu.server.modules.common.service.implement; + +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.common.entity.Tag; +import com.imyeyu.server.modules.common.mapper.TagMapper; +import com.imyeyu.server.modules.common.service.MultilingualService; +import com.imyeyu.server.modules.common.service.TagService; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +/** + * @author 夜雨 + * @since 2025-05-30 22:47 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class TagServiceImplement extends AbstractEntityService implements TagService { + + private final MultilingualService multilingualService; + + private final TagMapper mapper; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public void create(Tag tag) { + Long langId = multilingualService.createIfNotExist(UUID.randomUUID().toString(), tag.getValue()); + tag.setValue(String.valueOf(langId)); + super.create(tag); + } + + @Override + public List listByBizID(Tag.BizType bizType, String bizID) throws TimiException { + Tag example = new Tag(); + example.setBizType(bizType); + example.setBizID(bizID); + return mapper.selectAllByExample(example); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/implement/TaskServiceImplement.java b/src/main/java/com/imyeyu/server/modules/common/service/implement/TaskServiceImplement.java new file mode 100644 index 0000000..7cbfc0e --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/implement/TaskServiceImplement.java @@ -0,0 +1,28 @@ +package com.imyeyu.server.modules.common.service.implement; + +import com.imyeyu.server.modules.common.entity.Task; +import com.imyeyu.server.modules.common.mapper.TaskMapper; +import com.imyeyu.server.modules.common.service.TaskService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Comparator; +import java.util.List; + +/** + * 任务服务 + * + * @author 夜雨 + * @since 2022-04-03 15:37 + */ +@Service +@RequiredArgsConstructor +public class TaskServiceImplement implements TaskService { + + private final TaskMapper mapper; + + @Override + public List listAll4Public() { + return mapper.listAll4Public().stream().sorted(Comparator.comparingInt(c -> c.getStatus().getSort())).toList(); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/implement/TemplateServiceImplement.java b/src/main/java/com/imyeyu/server/modules/common/service/implement/TemplateServiceImplement.java new file mode 100644 index 0000000..4c8bfe8 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/implement/TemplateServiceImplement.java @@ -0,0 +1,31 @@ +package com.imyeyu.server.modules.common.service.implement; + +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.common.entity.Template; +import com.imyeyu.server.modules.common.mapper.TemplateMapper; +import com.imyeyu.server.modules.common.service.TemplateService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @author 夜雨 + * @since 2023-09-22 16:40 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class TemplateServiceImplement implements TemplateService { + + private final TemplateMapper mapper; + + @Override + public Template get(Template.BizType bizType, String bizCode) { + Template template = mapper.query(bizType, bizCode); + if (template == null) { + throw new TimiException(TimiCode.RESULT_NULL).msgKey("not found template"); + } + return template; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/implement/UserConfigServiceImplement.java b/src/main/java/com/imyeyu/server/modules/common/service/implement/UserConfigServiceImplement.java new file mode 100644 index 0000000..b68076c --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/implement/UserConfigServiceImplement.java @@ -0,0 +1,45 @@ +package com.imyeyu.server.modules.common.service.implement; + +import com.imyeyu.server.config.dbsource.TimiServerDBConfig; +import com.imyeyu.server.modules.common.entity.User; +import com.imyeyu.server.modules.common.entity.UserConfig; +import com.imyeyu.server.modules.common.mapper.UserConfigMapper; +import com.imyeyu.server.modules.common.service.UserConfigService; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 用户设置服务实现 + * + * @author 夜雨 + * @since 2021-08-12 16:24 + */ +@Service +@RequiredArgsConstructor(onConstructor_ = {@Lazy}) +public class UserConfigServiceImplement extends AbstractEntityService implements UserConfigService { + + private final UserService userService; + private final UserConfigMapper mapper; + + @Override + protected UserConfigMapper mapper() { + return mapper; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void update(UserConfig config) { + User user = userService.getLoginUser(); + + UserConfig dbConfig = get(user.getId()); + dbConfig.setUserId(user.getId()); + dbConfig.setEmailReplyRemind(config.isEmailReplyRemind()); + dbConfig.setUpdatedAt(Time.now()); + super.update(dbConfig); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/implement/UserPrivacyServiceImplement.java b/src/main/java/com/imyeyu/server/modules/common/service/implement/UserPrivacyServiceImplement.java new file mode 100644 index 0000000..3abee36 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/implement/UserPrivacyServiceImplement.java @@ -0,0 +1,50 @@ +package com.imyeyu.server.modules.common.service.implement; + +import com.imyeyu.server.config.dbsource.TimiServerDBConfig; +import com.imyeyu.server.modules.common.entity.User; +import com.imyeyu.server.modules.common.entity.UserPrivacy; +import com.imyeyu.server.modules.common.mapper.UserPrivacyMapper; +import com.imyeyu.server.modules.common.service.UserPrivacyService; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 用户隐私控制服务实现 + * + * @author 夜雨 + * @since 2021-07-27 17:19 + */ +@Service +@RequiredArgsConstructor +public class UserPrivacyServiceImplement extends AbstractEntityService implements UserPrivacyService { + + private final UserService userService; + private final UserPrivacyMapper mapper; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void update(UserPrivacy privacy) { + User user = userService.getLoginUser(); + + UserPrivacy dbPrivacy = get(user.getId()); + dbPrivacy.setEmail(privacy.isEmail()); + dbPrivacy.setSex(privacy.isSex()); + dbPrivacy.setBirthdate(privacy.isBirthdate()); + dbPrivacy.setQq(privacy.isQq()); + dbPrivacy.setLastLoginAt(privacy.isLastLoginAt()); + dbPrivacy.setCreatedAt(privacy.isCreatedAt()); + dbPrivacy.setUserId(user.getId()); + dbPrivacy.setUpdatedAt(Time.now()); + super.update(dbPrivacy); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/implement/UserProfileServiceImplement.java b/src/main/java/com/imyeyu/server/modules/common/service/implement/UserProfileServiceImplement.java new file mode 100644 index 0000000..ffda717 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/implement/UserProfileServiceImplement.java @@ -0,0 +1,105 @@ +package com.imyeyu.server.modules.common.service.implement; + +import com.imyeyu.io.IOSize; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.config.dbsource.TimiServerDBConfig; +import com.imyeyu.server.modules.common.entity.Attachment; +import com.imyeyu.server.modules.common.entity.User; +import com.imyeyu.server.modules.common.entity.UserProfile; +import com.imyeyu.server.modules.common.mapper.UserProfileMapper; +import com.imyeyu.server.modules.common.service.AttachmentService; +import com.imyeyu.server.modules.common.service.UserProfileService; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.server.modules.common.vo.attachment.AttachmentRequest; +import com.imyeyu.server.modules.common.vo.user.UserRequest; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +/** + * 用户数据服务实现 + * + * @author 夜雨 + * @since 2021-07-27 17:08 + */ +@Service +@RequiredArgsConstructor(onConstructor_ = {@Lazy}) +public class UserProfileServiceImplement extends AbstractEntityService implements UserProfileService { + + private final UserService userService; + private final AttachmentService attachmentService; + + private final UserProfileMapper mapper; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public void update(UserProfile profile) { + UserProfile dbProfile = get(profile.getUserId()); + dbProfile.setWrapperType(profile.getWrapperType()); + dbProfile.setAvatarType(profile.getAvatarType()); + dbProfile.setSex(profile.getSex()); + dbProfile.setBirthdate(profile.getBirthdate()); + dbProfile.setQq(profile.getQq()); + dbProfile.setDescription(profile.getDescription()); + super.update(dbProfile); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void update(UserRequest request) { + try { + User user = userService.getLoginUser(); + if (TimiJava.isNotEmpty(request.getProfile().getWrapper())) { + Attachment dbWrapper = attachmentService.getByAttachType(Attachment.BizType.USER, request.getId(), User.AttachType.WRAPPER); + // TODO 限制 PNG + if (dbWrapper != null) { + attachmentService.delete(dbWrapper.getId()); + } + MultipartFile wrapper = request.getProfile().getWrapper(); + // 字节数据 + byte[] bytes = wrapper.getInputStream().readAllBytes(); + if (IOSize.MB < bytes.length) { + throw new TimiException(TimiCode.ARG_BAD).msgKey("限制上传文件大小 1 MB"); + } + AttachmentRequest wrapperAttach = new AttachmentRequest(); + wrapperAttach.setName(request.getId() + ".png"); + wrapperAttach.setBizType(Attachment.BizType.USER); + wrapperAttach.setBizId(request.getId()); + wrapperAttach.setAttachTypeValue(User.AttachType.WRAPPER); + wrapperAttach.setSize(wrapper.getSize()); + wrapperAttach.setInputStream(wrapper.getInputStream()); + attachmentService.create(wrapperAttach); + } + if (TimiJava.isNotEmpty(request.getProfile().getAvatar())) { + Attachment dbAvatar = attachmentService.getByAttachType(Attachment.BizType.USER, request.getId(), User.AttachType.AVATAR); + if (dbAvatar != null) { + attachmentService.delete(dbAvatar.getId()); + } + MultipartFile avatar = request.getProfile().getAvatar(); + AttachmentRequest avatarAttach = new AttachmentRequest(); + avatarAttach.setName(request.getId() + ".png"); + avatarAttach.setBizType(Attachment.BizType.USER); + avatarAttach.setBizId(request.getId()); + avatarAttach.setAttachTypeValue(User.AttachType.AVATAR); + avatarAttach.setSize(avatar.getSize()); + avatarAttach.setInputStream(avatar.getInputStream()); + attachmentService.create(avatarAttach); + } + update(request.getProfile()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/implement/UserServiceImplement.java b/src/main/java/com/imyeyu/server/modules/common/service/implement/UserServiceImplement.java new file mode 100644 index 0000000..6b9456f --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/implement/UserServiceImplement.java @@ -0,0 +1,396 @@ +package com.imyeyu.server.modules.common.service.implement; + +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.config.dbsource.TimiServerDBConfig; +import com.imyeyu.server.modules.blog.util.UserToken; +import com.imyeyu.server.modules.common.entity.Attachment; +import com.imyeyu.server.modules.common.entity.EmailQueue; +import com.imyeyu.server.modules.common.entity.User; +import com.imyeyu.server.modules.common.entity.UserConfig; +import com.imyeyu.server.modules.common.entity.UserPrivacy; +import com.imyeyu.server.modules.common.entity.UserProfile; +import com.imyeyu.server.modules.common.mapper.CommentMapper; +import com.imyeyu.server.modules.common.mapper.CommentReplyMapper; +import com.imyeyu.server.modules.common.mapper.UserMapper; +import com.imyeyu.server.modules.common.mapper.UserProfileMapper; +import com.imyeyu.server.modules.common.service.AttachmentService; +import com.imyeyu.server.modules.common.service.EmailQueueService; +import com.imyeyu.server.modules.common.service.UserConfigService; +import com.imyeyu.server.modules.common.service.UserPrivacyService; +import com.imyeyu.server.modules.common.service.UserProfileService; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.server.modules.common.vo.user.LoginRequest; +import com.imyeyu.server.modules.common.vo.user.LoginResponse; +import com.imyeyu.server.modules.common.vo.user.RegisterRequest; +import com.imyeyu.server.modules.common.vo.user.UserProfileView; +import com.imyeyu.server.modules.common.vo.user.UserView; +import com.imyeyu.server.modules.git.entity.Developer; +import com.imyeyu.server.modules.git.service.DeveloperService; +import com.imyeyu.server.modules.minecraft.service.PlayerService; +import com.imyeyu.spring.TimiSpring; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.spring.util.Redis; +import com.imyeyu.utils.Calc; +import com.imyeyu.utils.Digest; +import com.imyeyu.utils.Text; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.UUID; + +/** + * 用户管理服务实现 + * + * @author 夜雨 + * @since 2021-02-23 21:43 + */ +@Slf4j +@Service +@RequiredArgsConstructor(onConstructor_ = {@Lazy}) +public class UserServiceImplement extends AbstractEntityService implements UserService, TimiJava { + + private final PlayerService mcPlayerService; + private final DeveloperService gitDeveloperService; + private final EmailQueueService emailQueueService; + private final UserConfigService configService; + private final AttachmentService attachmentService; + private final UserProfileService profileService; + private final UserPrivacyService privacyService; + + private final UserMapper mapper; + private final CommentMapper commentMapper; + private final UserProfileMapper userProfileMapper; + private final CommentReplyMapper commentReplyMapper; + + private final Redis redisUserExpFlag; + private final Redis redisUserEmailVerify; + private final Redis redisUserResetPWVerify; + + private final UserToken userToken; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void update(User user) { + User userByName = getByName(user.getName()); + if (userByName != null && !userByName.getId().equals(user.getId())) { + throw new TimiException(TimiCode.DATA_EXIST).msgKey("user.name.exist"); + } + User userByEmail = getByEmail(user.getEmail()); + if (userByEmail != null && !userByEmail.getId().equals(user.getId())) { + throw new TimiException(TimiCode.DATA_EXIST).msgKey("user.email.exist_and_verified"); + } + User dbUser = get(user.getId()); + if (TimiJava.isNotEmpty(user.getEmail())) { + if (!user.getEmail().equals(dbUser.getEmail())) { + // 重新验证 + dbUser.setEmailVerifyAt(null); + } + } + dbUser.setEmail(user.getEmail()); + dbUser.setName(user.getName()); + super.update(dbUser); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public LoginResponse register(RegisterRequest request) { + if (getByName(request.getName()) != null) { + throw new TimiException(TimiCode.DATA_EXIST).msgKey("user.register.exist_name"); + } + if (getByEmail(request.getEmail()) != null) { + throw new TimiException(TimiCode.DATA_EXIST).msgKey("user.register.exist_email"); + } + User user = new User(); + user.setEmail(request.getEmail()); + user.setName(request.getName()); + user.setPassword(digestPassword(request.getPassword(), Text.randomString(16))); + user.setCreatedAt(Time.now()); + // 注册账号 + create(user); + // 初始化资料 + profileService.create(new UserProfile(user.getId())); + // 初始化隐私控制 + privacyService.create(new UserPrivacy(user.getId())); + // 初始化设置 + configService.create(new UserConfig(user.getId())); + // 初始化开发者 + gitDeveloperService.create(new Developer(user.getId())); + // 自动登录 + return login(new LoginRequest(String.valueOf(user.getId()), request.getPassword())); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public LoginResponse login(LoginRequest request) { + if (TimiJava.isEmpty(request)) { + throw new TimiException(TimiCode.ARG_MISS); + } + // 用户 + if (TimiJava.isEmpty(request.getUser())) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("user.login.user.empty"); + } + // 密码 + if (TimiJava.isEmpty(request.getPassword())) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("user.login.password.empty"); + } + User result = get(request.getUser()); + if (request.getUser().contains("@")) { + if (!result.emailVerified()) { + throw new TimiException(TimiCode.RESULT_BAD).msgKey("user.login.email.not_verify"); + } + } + if (TimiJava.isEmpty(result)) { + throw new TimiException(TimiCode.RESULT_NULL).msgKey("user.login.not_found"); + } + if (result.isBanning()) { + throw new TimiException(TimiCode.RESULT_BAN).msgKey("user.login.baned"); + } + if (isInvalidPassword(result.getPassword(), request.getPassword())) { + throw new TimiException(TimiCode.RESULT_BAD).msgKey("user.login.password.mismatch"); + } + // 用户数据 + UserProfile profile = profileService.get(result.getId()); + profile.setLastLoginIP(TimiSpring.getRequestIP()); + profile.setLastLoginAt(Time.now()); + updateExp(profile.getUserId()); + userProfileMapper.update(profile); + // 生成并缓存 Token + String token = UUID.randomUUID().toString(); + Long expireAt = userToken.set(token, result.getId()); + return new LoginResponse(result.getId(), token, expireAt); + } + + @Override + public UserView view(Long userId) { + UserProfileView profile = new UserProfileView(); + { + // 附件 + List attachmentList = attachmentService.listByBizId(Attachment.BizType.USER, userId); + boolean hasAvatar = false, hasWrapper = false; + for (int i = 0; i < attachmentList.size(); i++) { + if (!hasAvatar && User.AttachType.AVATAR.toString().equals(attachmentList.get(i).getAttachType())) { + hasAvatar = true; + } + if (!hasWrapper && User.AttachType.WRAPPER.toString().equals(attachmentList.get(i).getAttachType())) { + hasWrapper = true; + } + } + if (!hasAvatar) { + attachmentList.add(attachmentService.getByAttachType(Attachment.BizType.USER, 0, User.AttachType.DEFAULT_AVATAR)); + } + if (!hasWrapper) { + attachmentList.add(attachmentService.getByAttachType(Attachment.BizType.USER, 0, User.AttachType.DEFAULT_WRAPPER)); + } + profile.setAttachmentList(attachmentList); + } + BeanUtils.copyProperties(profileService.get(userId), profile); + + UserView view = new UserView(); + view.setProfile(profile); + BeanUtils.copyProperties(get(userId), view); + return view; + } + + @Override + public boolean isInvalidPassword(String digestPassword, String password) { + // "$SHA$盐$摘要(摘要(明文) + 盐) + String[] arr = digestPassword.split("\\$"); + return digestPassword(digestPassword, arr[2]).equals(password); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public LoginResponse login4Token() { + String token = TimiSpring.getToken(); + try { + User user = getLoginUser(); + updateExp(user.getId()); + return new LoginResponse(user.getId(), token, userToken.getExpireAt(token)); + } catch (Exception e) { + throw new TimiException(TimiCode.IGNORE); + } + } + + @Override + public void logout() { + userToken.clear(TimiSpring.getToken()); + } + + @Override + public User get(String user) { + User result; + if (Calc.isNumber(user)) { + result = get(Long.parseLong(user)); + } else if (user.contains("@")) { + result = getByEmail(user); + } else { + result = getByName(user); + } + return result; + } + + @Override + public boolean isLogged() { + return userToken.isValid(TimiSpring.getToken()); + } + + @Override + public User getLoginUser() { + String token = TimiSpring.getToken(); + if (TimiJava.isEmpty(token)) { + return null; + } + return userToken.getUser(token); + } + + @Override + public User getByName(String name) { + return mapper.selectByName(name); + } + + @Override + public User getByEmail(String email) { + return mapper.selectByEmail(email); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void sendEmailVerify() { + User user = getLoginUser(); + if (TimiJava.isEmpty(user.getEmail())) { + throw new TimiException(TimiCode.RESULT_BAD).msgKey("user.email.empty"); + } + if (user.emailVerified()) { + throw new TimiException(TimiCode.RESULT_BAD).msgKey("user.email.verified"); + } + EmailQueue emailQueue = new EmailQueue(); + emailQueue.setBizType(EmailQueue.BizType.EMAIL_VERIFY); + emailQueue.setBizId(user.getId()); + emailQueue.setSendAt(Time.now()); + emailQueueService.create(emailQueue); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void emailVerifyCallback(String key) { + User user = getLoginUser(); + if (user.emailVerified()) { + throw new TimiException(TimiCode.RESULT_BAD).msgKey("user.email.verified"); + } + Long userId = redisUserEmailVerify.get(key); + if (userId == null) { + throw new TimiException(TimiCode.ARG_BAD).msgKey("user.email.illegal_key"); + } + if (!userId.equals(user.getId())) { + throw new TimiException(TimiCode.ARG_BAD).msgKey("user.email.illegal_key"); + } + redisUserEmailVerify.destroy(key); + user.setEmailVerifyAt(null); + update(user); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void updatePassword(String oldPassword, String newPassword) { + String token = TimiSpring.getToken(); + User user = getLoginUser(); + if (isInvalidPassword(user.getPassword(), oldPassword)) { + throw new TimiException(TimiCode.RESULT_BAD).msgKey("user.password.mismatch"); + } + // 更新密码 + user.setPassword(digestPassword(newPassword, Text.randomString(16))); + user.setUpdatedAt(Time.now()); + mapper.update(user); + // 清除登录会话 + userToken.clear(token); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void sendPasswordForgetVerify(String user) { + User result = get(user); + if (result == null) { + throw new TimiException(TimiCode.RESULT_NULL).msgKey("user.password.forget.not_found"); + } + if (TimiJava.isEmpty(result.getEmail()) || !result.emailVerified()) { + throw new TimiException(TimiCode.RESULT_NULL).msgKey("user.password.forget.not_valid_email"); + } + EmailQueue emailQueue = new EmailQueue(); + emailQueue.setBizType(EmailQueue.BizType.RESET_PASSWORD); + emailQueue.setBizId(result.getId()); + emailQueue.setSendAt(Time.now()); + emailQueueService.create(emailQueue); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void resetPasswordByKey(String key, String password) { + Long userId = redisUserResetPWVerify.get(key); + if (userId == null) { + throw new TimiException(TimiCode.ARG_BAD).msgKey("user.password.forget.illegal_key"); + } + User user = get(userId); + redisUserResetPWVerify.destroy(key); + user.setPassword(digestPassword(password, Text.randomString(16))); + user.setUpdatedAt(Time.now()); + mapper.update(user); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void cancel(String password) { + String token = TimiSpring.getToken(); + User user = getLoginUser(); + if (isInvalidPassword(user.getPassword(), password)) { + throw new TimiException(TimiCode.ARG_BAD).msgKey("user.cancel.password_error"); + } + // 删除评论 + commentMapper.deleteByUserId(user.getId()); + // 删除回复 + commentReplyMapper.deleteByUserId(user.getId()); + // 删除账号 + delete(user.getId()); + // 清除登录会话 + userToken.clear(token); + } + + private void updateExp(Long userId) { + if (!redisUserExpFlag.has(userId)) { + // 当天无登录标记,加经验 + UserProfile profile = profileService.get(userId); + profile.setExp(profile.getExp() + 2); + redisUserExpFlag.set(profile.getUserId(), "", Time.tomorrow() - Time.now()); + profileService.update(profile); + } + } + + /** + * 生成密码摘要 + * + * @param password 原始密码 + * @param salt 盐值 + * @return 密码摘要 + */ + private String digestPassword(String password, String salt) { + try { + return "$SHA$%s$%s".formatted(salt, Digest.sha256(Digest.sha256(password) + salt)); + } catch (Exception e) { + log.error("digest password error", e); + throw new TimiException(TimiCode.ERROR).msgKey("TODO digest password error"); + } + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/service/implement/VersionServiceImplement.java b/src/main/java/com/imyeyu/server/modules/common/service/implement/VersionServiceImplement.java new file mode 100644 index 0000000..415a081 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/service/implement/VersionServiceImplement.java @@ -0,0 +1,25 @@ +package com.imyeyu.server.modules.common.service.implement; + +import lombok.RequiredArgsConstructor; +import com.imyeyu.server.modules.common.entity.Version; +import com.imyeyu.server.modules.common.mapper.VersionMapper; +import com.imyeyu.server.modules.common.service.VersionService; +import org.springframework.stereotype.Service; + +/** + * 版本服务实现 + * + * @author 夜雨 + * @since 2021-06-10 16:07 + */ +@Service +@RequiredArgsConstructor +public class VersionServiceImplement implements VersionService { + + private final VersionMapper mapper; + + @Override + public Version getByName(String name) { + return mapper.queryByName(name); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/task/EmailTask.java b/src/main/java/com/imyeyu/server/modules/common/task/EmailTask.java new file mode 100644 index 0000000..f0dde87 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/task/EmailTask.java @@ -0,0 +1,222 @@ +package com.imyeyu.server.modules.common.task; + +import freemarker.template.Template; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.server.config.dbsource.TimiServerDBConfig; +import com.imyeyu.server.modules.blog.entity.CommentRemindQueue; +import com.imyeyu.server.modules.blog.service.CommentRemindQueueService; +import com.imyeyu.server.modules.common.bean.EmailException; +import com.imyeyu.server.modules.common.entity.CommentReply; +import com.imyeyu.server.modules.common.entity.EmailQueue; +import com.imyeyu.server.modules.common.entity.EmailQueueLog; +import com.imyeyu.server.modules.common.entity.User; +import com.imyeyu.server.modules.common.service.CommentReplyService; +import com.imyeyu.server.modules.common.service.EmailQueueService; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.server.modules.common.vo.comment.CommentReplyView; +import com.imyeyu.spring.util.Redis; +import com.imyeyu.utils.Text; +import com.imyeyu.utils.Time; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.ui.freemarker.FreeMarkerTemplateUtils; +import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 邮件推送任务 + * + * @author 夜雨 + * @since 2021-08-24 14:10 + */ +@Slf4j +@Configuration +@EnableScheduling +@RequiredArgsConstructor +public class EmailTask implements TimiJava { + + @Value("${spring.profiles.active}") + private String env; + + @Value("${spring.mail.username}") + private String sendUser; + + private final UserService userService; + private final JavaMailSender mailSender; + private final EmailQueueService service; + private final CommentReplyService commentReplyService; + private final CommentRemindQueueService commentRemindQueueService; + + private final Redis redisUserEmailVerify; + private final Redis redisUserResetPWVerify; + + private final FreeMarkerConfigurer freeMarkerConfigurer; + + @Scheduled(fixedRate = Time.S * 8) + @Transactional(TimiServerDBConfig.ROLLBACKER) + public void traverseQueue() { + List emailQueueList = service.listAll(); + if (TimiJava.isNotEmpty(emailQueueList)) { + for (EmailQueue emailQueue : emailQueueList) { + long now = System.currentTimeMillis(); + if (emailQueue.getSendAt() < now) { + log.info(emailQueue.getUUID() + " 邮件推送:" + emailQueue.getBizType() + "." +emailQueue.getBizId()); + + EmailQueueLog emailQueueLog = new EmailQueueLog(); + emailQueueLog.setUUID(emailQueue.getUUID()); + emailQueueLog.setBizType(emailQueue.getBizType()); + emailQueueLog.setBizId(emailQueue.getBizId()); + emailQueueLog.setSendAt(emailQueue.getSendAt()); + try { + String sendTo = switch (emailQueue.getBizType()) { + case REPLY_REMINAD -> sendEmail4ReplyRemind(emailQueue); + case EMAIL_VERIFY -> sendEmail4EmailVerify(emailQueue); + case RESET_PASSWORD -> sendEmail4ResetPassword(emailQueue); + }; + emailQueueLog.setSendTo(sendTo); + emailQueueLog.setIsSent(true); + emailQueueLog.setCreatedAt(System.currentTimeMillis()); + log.info(emailQueue.getUUID() + " 邮件 " + emailQueueLog.getSendTo() + " 推送成功:" + (emailQueueLog.getCreatedAt() - now) + " ms"); + } catch (EmailException e) { + emailQueueLog.setIsSent(false); + log.error(emailQueue.getUUID() + " 邮件 " + e.getEmail() + " 推送中止:", e.getMessage()); + emailQueueLog.setExceptionMsg(e.getMessage()); + } catch (Exception e) { + emailQueueLog.setIsSent(false); + log.error(emailQueue.getUUID() + " 邮件 " + emailQueueLog.getSendTo() + " 推送异常", e); + emailQueueLog.setExceptionMsg(e.getMessage().substring(0,Math.min(200, e.getMessage().length())) + "..."); + } finally { + emailQueueLog.setCreatedAt(now); + service.addLog(emailQueueLog); + service.destroy(emailQueue.getUUID()); + } + } + } + } + } + + /** + * 发送邮件 + * + * @param to 目标邮箱 + * @param subject 标题 + * @param html HTML 字符串 + * @throws Exception 发送异常 + */ + private void sendEmail(String to, String subject, String html) throws Exception { + if (env.contains("dev")) { + log.info("skip send email in debug environment"); + log.info("send title: {}", subject); + log.info("send to: {}", to); + log.info("send detail: \n{}", html); + return; + } + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true); + helper.setFrom(sendUser); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(html, true); + mailSender.send(message); + } + + /** + * 邮箱验证 + * + * @param emailQueue 邮件队列 + * @return 发送目标 + * @throws Exception 服务异常 + */ + private String sendEmail4EmailVerify(EmailQueue emailQueue) throws Exception { + User user = userService.get(emailQueue.getBizId()); //.withData(); + + String key = Text.randomString(64); + redisUserEmailVerify.set(key, user.getId(), 600L); + + Map model = new HashMap<>(); + + model.put("user", user); + model.put("url", "https://www.imyeyu.net/user/space/%s?action=EMAIL_VERIFY&key=%s".formatted(user.getId(), key)); + + Template template = freeMarkerConfigurer.getConfiguration().getTemplate("EmailVerify.ftl"); + String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, model); + + sendEmail(user.getEmail(), "Hey! 请继续完成 夜雨博客 的邮箱验证!", html); + return user.getEmail(); + } + + /** + * 回复提醒邮件 + * + * @param emailQueue 邮件队列 + * @return 发送目标 + * @throws Exception 服务异常 + */ + private String sendEmail4ReplyRemind(EmailQueue emailQueue) throws Exception { + User user = userService.get(emailQueue.getBizId()); // TODO .withData(); + List reminds = commentRemindQueueService.listByUserId(emailQueue.getBizId()); + if (reminds.isEmpty()) { + throw new EmailException(TimiCode.RESULT_NULL, "没有需要提醒的回复", user.getEmail()); + } + // 回查数据 + for (CommentRemindQueue remind : reminds) { + // 回复 + CommentReply reply = commentReplyService.get(remind.getReplyId()); + CommentReplyView replyView = new CommentReplyView(); + BeanUtils.copyProperties(reply, replyView); + remind.setReply(replyView); + if (TimiJava.isNotEmpty(remind.getReply().getSenderId())) { + // 发送者 + remind.getReply().setSender(userService.view(remind.getReply().getSenderId())); + } + } + + Map model = new HashMap<>(); + model.put("user", user); + model.put("reminds", reminds); + + Template template = freeMarkerConfigurer.getConfiguration().getTemplate("ReplyRemind.ftl"); + String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, model); + + sendEmail(user.getEmail(), "Hey! 你在 夜雨博客 的评论收到新回复", html); + // 移除回复提醒队列 + commentRemindQueueService.destroyByUserId(emailQueue.getBizId()); + return user.getEmail(); + } + + /** + * 重置密码验证 + * + * @param emailQueue 邮件队列 + * @return 发送目标 + * @throws Exception 服务异常 + */ + private String sendEmail4ResetPassword(EmailQueue emailQueue) throws Exception { + User user = userService.get(emailQueue.getBizId()); // TODO .withData(); + + String key = Text.randomString(64); + redisUserResetPWVerify.set(key, user.getId(), 600L); + + Map model = new HashMap<>(); + model.put("user", user); + model.put("url", "https://www.imyeyu.net/user/pw-reset?key=" + key); + + Template template = freeMarkerConfigurer.getConfiguration().getTemplate("ResetPassword.ftl"); + String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, model); + sendEmail(user.getEmail(), "Hey! 请继续完成 夜雨博客 的重置密码!", html); + return user.getEmail(); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/task/MultilingualTranslateTask.java b/src/main/java/com/imyeyu/server/modules/common/task/MultilingualTranslateTask.java new file mode 100644 index 0000000..94a00fb --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/task/MultilingualTranslateTask.java @@ -0,0 +1,166 @@ +package com.imyeyu.server.modules.common.task; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.Language; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.java.ref.Ref; +import com.imyeyu.network.FormMap; +import com.imyeyu.server.config.dbsource.TimiServerDBConfig; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.entity.Multilingual; +import com.imyeyu.server.modules.common.service.MultilingualService; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.utils.Digest; +import com.imyeyu.utils.Time; +import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.client5.http.fluent.Request; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author 夜雨 + * @since 2025-05-31 11:06 + */ +@Slf4j +@Configuration +@EnableScheduling +@RequiredArgsConstructor +public class MultilingualTranslateTask { + + /** + * + * + * @author 夜雨 + * @since 2025-05-31 11:08 + */ + @AllArgsConstructor + public enum BaiduLanguage { + + ZH(Language.zh_CN), + EN(Language.en_US), + JP(Language.ja_JP), + KOR(Language.ko_KR), + RU(Language.ru_RU), + DE(Language.de_DE), + CHT(Language.zh_TW); + + /** 标准映射 */ + final Language language; + + /** + * 获取排除语言列表 + * + * @param baiduLanguage 排除语言 + * @return 语言列表 + */ + static List valuesWithout(BaiduLanguage... baiduLanguage) { + Set outList = Set.of(baiduLanguage); + + List result = new ArrayList<>(); + BaiduLanguage[] values = values(); + for (int i = 0; i < values.length; i++) { + if (!outList.contains(values[i])) { + result.add(values[i]); + } + } + return result; + } + } + + private final SettingService settingService; + private final MultilingualService service; + + @Scheduled(fixedRate = Time.M * 10) + @Transactional(TimiServerDBConfig.ROLLBACKER) + public void handle() { + try { + List list = service.listByNotTranslate(); + + Map cnMap = new HashMap<>(); + for (int i = 0; i < list.size(); i++) { + StringBuilder sb = new StringBuilder(); + for (int j = 0; j < Math.min(list.size() - i, 20); j++, i++) { + Multilingual multilingual = list.get(i); + sb.append(multilingual.getZhCN()).append("\r\n"); + cnMap.put(multilingual.getZhCN(), multilingual); + } + i--; + List languageList = BaiduLanguage.valuesWithout(BaiduLanguage.ZH); + for (int j = 0; j < languageList.size(); j++) { + Map result = doTranslate(sb.toString(), languageList.get(j)); + for (Map.Entry item : result.entrySet()) { + Multilingual multilingual = cnMap.get(item.getKey()); + Language lang = languageList.get(j).language; + String value = multilingual.getValue(lang); + if (TimiJava.isEmpty(value)) { + Ref.setFieldValue(multilingual, lang.toString().replace("_", ""), item.getValue()); + } + service.update(multilingual); + } + } + wait(1000); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + /** + * 文本翻译 + * + * @param text 原文本 + * @param to 目标语言 + * @return Map<原数据,翻译结果> + * @throws Exception 翻译异常 + */ + private synchronized Map doTranslate(String text, BaiduLanguage to) throws Exception { + String random = String.valueOf(Time.now()); + + String appId = settingService.getAsString(SettingKey.MULTILINGUAL_TRANSLATE_APP_ID); + String key = settingService.getAsString(SettingKey.MULTILINGUAL_TRANSLATE_KEY); + + FormMap args = new FormMap<>(); + args.put("q", text); + args.put("from", BaiduLanguage.ZH.toString().toLowerCase()); + args.put("to", to.toString().toLowerCase()); + args.put("appid", appId); + args.put("salt", random); + args.put("sign", Digest.md5(appId + text + random + key)); + + String response = Request.post(settingService.getAsString(SettingKey.MULTILINGUAL_TRANSLATE_API)) + .bodyForm(args.build()) + .execute() + .returnContent() + .asString(); + JsonObject jo = JsonParser.parseString(response).getAsJsonObject(); + if (jo.has("error_code")) { + System.err.println(jo); + throw new TimiException(TimiCode.ERROR, jo.get("error_msg").getAsString()); + } + JsonArray ja = jo.get("trans_result").getAsJsonArray(); + + JsonObject resultJO; + Map result = new HashMap<>(); + for (int i = 0; i < ja.size(); i++) { + resultJO = ja.get(i).getAsJsonObject(); + result.put(resultJO.get("src").getAsString(), resultJO.get("dst").getAsString()); + } + wait(200); + return result; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/validation/UserName.java b/src/main/java/com/imyeyu/server/modules/common/validation/UserName.java new file mode 100644 index 0000000..ddf90cb --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/validation/UserName.java @@ -0,0 +1,36 @@ +package com.imyeyu.server.modules.common.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import com.imyeyu.server.modules.common.validation.validtor.UserNameValidator; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * + * + * @author 夜雨 + * @since 2023-05-06 18:01 + */ +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = UserNameValidator.class) +public @interface UserName { + + String message() default ""; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/validation/UserPassword.java b/src/main/java/com/imyeyu/server/modules/common/validation/UserPassword.java new file mode 100644 index 0000000..c50308b --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/validation/UserPassword.java @@ -0,0 +1,34 @@ +package com.imyeyu.server.modules.common.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import com.imyeyu.server.modules.common.validation.validtor.UserPasswordValidator; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @author 夜雨 + * @since 2023-05-07 00:05 + */ +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = UserPasswordValidator.class) +public @interface UserPassword { + + String message() default ""; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/validation/validtor/UserNameValidator.java b/src/main/java/com/imyeyu/server/modules/common/validation/validtor/UserNameValidator.java new file mode 100644 index 0000000..bb3a3ff --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/validation/validtor/UserNameValidator.java @@ -0,0 +1,35 @@ +package com.imyeyu.server.modules.common.validation.validtor; + +import com.imyeyu.java.TimiJava; +import com.imyeyu.server.modules.common.validation.UserName; +import com.imyeyu.spring.util.AbstractValidator; +import com.imyeyu.utils.Text; + +/** + * 用户名基本验证 + * + * @author 夜雨 + * @since 2023-05-06 18:01 + */ +public class UserNameValidator extends AbstractValidator { + + @Override + protected String inspector(String userName) { + if (TimiJava.isEmpty(userName)) { + return "user.name.empty"; + } + if (32 < userName.length()) { + return "user.name.too_long"; + } + if (userName.contains("@")) { + return "user.name.contains_at"; + } + if (Text.testReg("^[0-9]+.?[0-9]*$", userName)) { + return "user.name.only_number"; + } + if (!Text.testReg("^[A-Za-z0-9_一-龥]+$", userName)) { + return "user.name.not_match_regex"; + } + return null; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/validation/validtor/UserPasswordValidator.java b/src/main/java/com/imyeyu/server/modules/common/validation/validtor/UserPasswordValidator.java new file mode 100644 index 0000000..79094fe --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/validation/validtor/UserPasswordValidator.java @@ -0,0 +1,32 @@ +package com.imyeyu.server.modules.common.validation.validtor; + +import com.imyeyu.java.TimiJava; +import com.imyeyu.server.modules.common.validation.UserPassword; +import com.imyeyu.spring.util.AbstractValidator; +import com.imyeyu.utils.Text; + +/** + * 用户密码基本验证 + * + * @author 夜雨 + * @since 2023-05-06 18:01 + */ +public class UserPasswordValidator extends AbstractValidator { + + @Override + protected String inspector(String password) { + if (TimiJava.isEmpty(password)) { + return "user.password.empty"; + } + if (password.length() < 6) { + return "user.password.too_short"; + } + if (20 < password.length()) { + return "user.password.too_long"; + } + if (!Text.testReg("(?=.*([a-zA-Z].*))(?=.*[0-9].*)[a-zA-Z0-9-*/+.~!@#$%^&*()]{6,20}$", password)) { + return "user.password.not_match_regex"; + } + return null; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/CaptchaRequest.java b/src/main/java/com/imyeyu/server/modules/common/vo/CaptchaRequest.java new file mode 100644 index 0000000..5f6fc5b --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/CaptchaRequest.java @@ -0,0 +1,20 @@ +package com.imyeyu.server.modules.common.vo; + +import lombok.Data; +import com.imyeyu.server.bean.CaptchaFrom; + +/** + * 验证码请求,不要执行参数校验,否则会被全局拦截器统一返回 JSON 错误,但这是验证码,需要返回错误图片 + * + * @author 夜雨 + * @since 2023-07-15 18:14 + */ +@Data +public class CaptchaRequest { + + private int width; + + private int height; + + private CaptchaFrom from; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/FeedbackRequest.java b/src/main/java/com/imyeyu/server/modules/common/vo/FeedbackRequest.java new file mode 100644 index 0000000..d733878 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/FeedbackRequest.java @@ -0,0 +1,22 @@ +package com.imyeyu.server.modules.common.vo; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * @author 夜雨 + * @since 2023-05-26 13:21 + */ +@Data +public class FeedbackRequest { + + @NotBlank + private String from; + + @Email + private String email; + + @NotBlank + private String data; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/attachment/AttachmentRequest.java b/src/main/java/com/imyeyu/server/modules/common/vo/attachment/AttachmentRequest.java new file mode 100644 index 0000000..24c9b49 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/attachment/AttachmentRequest.java @@ -0,0 +1,21 @@ +package com.imyeyu.server.modules.common.vo.attachment; + +import com.imyeyu.server.modules.common.entity.Attachment; +import com.imyeyu.spring.annotation.table.Transient; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.InputStream; + + +/** + * @author 夜雨 + * @since 2024-02-21 10:58 + */ +@Data +@Transient +@EqualsAndHashCode(callSuper = true) +public class AttachmentRequest extends Attachment { + + private InputStream inputStream; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/attachment/AttachmentView.java b/src/main/java/com/imyeyu/server/modules/common/vo/attachment/AttachmentView.java new file mode 100644 index 0000000..70e2172 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/attachment/AttachmentView.java @@ -0,0 +1,15 @@ +package com.imyeyu.server.modules.common.vo.attachment; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.server.bean.MultilingualHandler; +import com.imyeyu.server.modules.common.entity.Attachment; + +/** + * @author 夜雨 + * @since 2024-02-21 10:55 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class AttachmentView extends Attachment implements MultilingualHandler { +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/comment/CommentReplyPage.java b/src/main/java/com/imyeyu/server/modules/common/vo/comment/CommentReplyPage.java new file mode 100644 index 0000000..c5575cd --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/comment/CommentReplyPage.java @@ -0,0 +1,41 @@ +package com.imyeyu.server.modules.common.vo.comment; + +import com.imyeyu.spring.bean.Page; +import jakarta.validation.constraints.Min; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +/** + * @author 夜雨 + * @since 2023-07-15 09:03 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class CommentReplyPage extends Page { + + /** + * + * + * @author 夜雨 + * @since 2025-04-17 23:29 + */ + @Getter + @AllArgsConstructor + public enum BizType { + + COMMENT("comment_id"), + + SENDER("sender_id"), + + RECEIVER("receiver_id"); + + final String column; + } + + private BizType bizType; + + @Min(1) + private Long bizId; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/comment/CommentReplyView.java b/src/main/java/com/imyeyu/server/modules/common/vo/comment/CommentReplyView.java new file mode 100644 index 0000000..3fd2802 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/comment/CommentReplyView.java @@ -0,0 +1,28 @@ +package com.imyeyu.server.modules.common.vo.comment; + +import com.imyeyu.server.modules.common.entity.CommentReply; +import com.imyeyu.server.modules.common.vo.user.UserView; +import com.imyeyu.spring.annotation.table.Transient; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * @author 夜雨 + * @since 2024-03-05 17:48 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class CommentReplyView extends CommentReply { + + /** 所属评论 */ + @Transient + private CommentView comment; + + /** 发送用户 */ + @Transient + private UserView sender; + + /** 回复用户 */ + @Transient + private UserView receiver; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/comment/CommentView.java b/src/main/java/com/imyeyu/server/modules/common/vo/comment/CommentView.java new file mode 100644 index 0000000..aa62b3c --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/comment/CommentView.java @@ -0,0 +1,34 @@ +package com.imyeyu.server.modules.common.vo.comment; + +import com.imyeyu.server.modules.blog.entity.Article; +import com.imyeyu.server.modules.common.entity.Comment; +import com.imyeyu.server.modules.common.vo.user.UserView; +import com.imyeyu.server.modules.git.bean.gitea.Repository; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2024-02-29 16:37 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class CommentView extends Comment { + + /** 回复数量 */ + private int repliesLength; + + /** 发送用户 */ + private UserView user; + + /** 关联文章 */ + private Article article; + + /** 关联仓库 */ + private Repository repository; + + /** 回复列表 */ + private List replies; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/comment/UserCommentPage.java b/src/main/java/com/imyeyu/server/modules/common/vo/comment/UserCommentPage.java new file mode 100644 index 0000000..5afdcb0 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/comment/UserCommentPage.java @@ -0,0 +1,18 @@ +package com.imyeyu.server.modules.common.vo.comment; + +import jakarta.validation.constraints.Min; +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.bean.Page; + +/** + * @author 夜雨 + * @since 2023-07-15 14:13 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class UserCommentPage extends Page { + + @Min(1) + private Long userId; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/icon/AllResponse.java b/src/main/java/com/imyeyu/server/modules/common/vo/icon/AllResponse.java new file mode 100644 index 0000000..18469f6 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/icon/AllResponse.java @@ -0,0 +1,18 @@ +package com.imyeyu.server.modules.common.vo.icon; + +import com.imyeyu.server.modules.common.entity.Icon; +import lombok.Data; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2023-09-11 12:41 + */ +@Data +public class AllResponse { + + private long latest; + + private List icons; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/icon/LabelPage.java b/src/main/java/com/imyeyu/server/modules/common/vo/icon/LabelPage.java new file mode 100644 index 0000000..fdaeccc --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/icon/LabelPage.java @@ -0,0 +1,18 @@ +package com.imyeyu.server.modules.common.vo.icon; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.bean.Page; + +/** + * @author 夜雨 + * @since 2023-07-14 15:26 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class LabelPage extends Page { + + @NotBlank(message = "icon.page.label.empty") + private String label; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/icon/NamePage.java b/src/main/java/com/imyeyu/server/modules/common/vo/icon/NamePage.java new file mode 100644 index 0000000..71ee0bb --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/icon/NamePage.java @@ -0,0 +1,18 @@ +package com.imyeyu.server.modules.common.vo.icon; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.bean.Page; + +/** + * @author 夜雨 + * @since 2023-07-14 16:13 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NamePage extends Page { + + @NotBlank(message = "icon.page.name.empty") + private String name; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/icon/UnicodePage.java b/src/main/java/com/imyeyu/server/modules/common/vo/icon/UnicodePage.java new file mode 100644 index 0000000..f45eb24 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/icon/UnicodePage.java @@ -0,0 +1,18 @@ +package com.imyeyu.server.modules.common.vo.icon; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.bean.Page; + +/** + * @author 夜雨 + * @since 2023-07-14 15:00 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class UnicodePage extends Page { + + @NotBlank(message = "icon.page.unicode.empty") + private String unicode; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/tag/TagRequest.java b/src/main/java/com/imyeyu/server/modules/common/vo/tag/TagRequest.java new file mode 100644 index 0000000..80cb9fe --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/tag/TagRequest.java @@ -0,0 +1,15 @@ +package com.imyeyu.server.modules.common.vo.tag; + +import lombok.Data; + +/** + * @author 夜雨 + * @since 2025-05-30 22:58 + */ +@Data +public class TagRequest { + + private String bizId; + + private String zhCN; +} \ No newline at end of file diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/user/EmailVerifyCallbackRequest.java b/src/main/java/com/imyeyu/server/modules/common/vo/user/EmailVerifyCallbackRequest.java new file mode 100644 index 0000000..06d6bf8 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/user/EmailVerifyCallbackRequest.java @@ -0,0 +1,15 @@ +package com.imyeyu.server.modules.common.vo.user; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * @author 夜雨 + * @since 2023-07-18 17:37 + */ +@Data +public class EmailVerifyCallbackRequest { + + @NotBlank + private String key; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/user/LoginRequest.java b/src/main/java/com/imyeyu/server/modules/common/vo/user/LoginRequest.java new file mode 100644 index 0000000..9fd201b --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/user/LoginRequest.java @@ -0,0 +1,25 @@ +package com.imyeyu.server.modules.common.vo.user; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import com.imyeyu.server.modules.common.validation.UserPassword; + +/** + * 登录请求对象 + * + * @author 夜雨 + * @since 2023-04-25 17:26 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class LoginRequest { + + /** 用户(可能是 UID、邮箱或用户名) */ + private String user; + + /** 明文密码 */ + @UserPassword + private String password; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/user/LoginResponse.java b/src/main/java/com/imyeyu/server/modules/common/vo/user/LoginResponse.java new file mode 100644 index 0000000..d244a10 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/user/LoginResponse.java @@ -0,0 +1,26 @@ +package com.imyeyu.server.modules.common.vo.user; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 登录返回对象 + * + * @author 夜雨 + * @since 2023-05-05 17:57 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class LoginResponse { + + /** 登录用户 ID */ + private Long id; + + /** 令牌 */ + private String token; + + /** 过期时间 */ + private Long expireAt; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/user/RegisterRequest.java b/src/main/java/com/imyeyu/server/modules/common/vo/user/RegisterRequest.java new file mode 100644 index 0000000..231eb6d --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/user/RegisterRequest.java @@ -0,0 +1,32 @@ +package com.imyeyu.server.modules.common.vo.user; + +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import com.imyeyu.server.modules.common.validation.UserName; +import com.imyeyu.server.modules.common.validation.UserPassword; + +/** + * 注册请求对象 + * + * @author 夜雨 + * @since 2023-05-05 18:08 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class RegisterRequest { + + /** 用户名 */ + @UserName + private String name; + + /** 明文密码 */ + @UserPassword + private String password; + + /** 邮箱 */ + @Pattern(regexp = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$", message = "user.email.not_match_regex") + private String email; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/user/UpdatePasswordByKeyRequest.java b/src/main/java/com/imyeyu/server/modules/common/vo/user/UpdatePasswordByKeyRequest.java new file mode 100644 index 0000000..6559904 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/user/UpdatePasswordByKeyRequest.java @@ -0,0 +1,19 @@ +package com.imyeyu.server.modules.common.vo.user; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import com.imyeyu.server.modules.common.validation.UserPassword; + +/** + * @author 夜雨 + * @since 2023-07-15 11:23 + */ +@Data +public class UpdatePasswordByKeyRequest { + + @NotBlank + private String key; + + @UserPassword + private String newPassword; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/user/UpdatePasswordRequest.java b/src/main/java/com/imyeyu/server/modules/common/vo/user/UpdatePasswordRequest.java new file mode 100644 index 0000000..ae9eabb --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/user/UpdatePasswordRequest.java @@ -0,0 +1,18 @@ +package com.imyeyu.server.modules.common.vo.user; + +import lombok.Data; +import com.imyeyu.server.modules.common.validation.UserPassword; + +/** + * @author 夜雨 + * @since 2023-07-15 11:23 + */ +@Data +public class UpdatePasswordRequest { + + @UserPassword + private String oldValue; + + @UserPassword + private String newValue; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/user/UserProfileView.java b/src/main/java/com/imyeyu/server/modules/common/vo/user/UserProfileView.java new file mode 100644 index 0000000..da0eaed --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/user/UserProfileView.java @@ -0,0 +1,21 @@ +package com.imyeyu.server.modules.common.vo.user; + +import com.imyeyu.server.modules.common.entity.Attachment; +import com.imyeyu.server.modules.common.entity.UserProfile; +import com.imyeyu.spring.annotation.table.Transient; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2024-02-21 13:00 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class UserProfileView extends UserProfile { + + @Transient + private List attachmentList; +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/user/UserRequest.java b/src/main/java/com/imyeyu/server/modules/common/vo/user/UserRequest.java new file mode 100644 index 0000000..719e76a --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/user/UserRequest.java @@ -0,0 +1,33 @@ +package com.imyeyu.server.modules.common.vo.user; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.server.modules.common.entity.User; +import com.imyeyu.server.modules.common.entity.UserProfile; +import org.springframework.web.multipart.MultipartFile; + +/** + * @author 夜雨 + * @since 2024-02-21 11:55 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class UserRequest extends User { + + private UserProfileRequest profile; + + /** + * + * + * @author 夜雨 + * @since 2024-05-17 00:06 + */ + @Data + @EqualsAndHashCode(callSuper = true) + public static final class UserProfileRequest extends UserProfile { + + private MultipartFile wrapper; + + private MultipartFile avatar; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/common/vo/user/UserView.java b/src/main/java/com/imyeyu/server/modules/common/vo/user/UserView.java new file mode 100644 index 0000000..9e2cdbf --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/common/vo/user/UserView.java @@ -0,0 +1,56 @@ +package com.imyeyu.server.modules.common.vo.user; + +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.java.ref.Ref; +import com.imyeyu.server.TimiServerAPI; +import com.imyeyu.server.modules.common.entity.User; +import com.imyeyu.server.modules.common.entity.UserPrivacy; +import com.imyeyu.server.modules.common.service.UserPrivacyService; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.spring.annotation.table.Transient; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * 用户视图 + * + * @author 夜雨 + * @since 2024-03-05 20:00 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class UserView extends User { + + /** 用户信息视图 */ + @Transient + private UserProfileView profile; + + public UserView doFilter() { + try { + UserService userService = TimiServerAPI.applicationContext.getBean(UserService.class); + UserPrivacyService privacyService = TimiServerAPI.applicationContext.getBean(UserPrivacyService.class); + + password = null; + updatedAt = null; + emailVerifyAt = null; + + profile.setUpdatedAt(null); + profile.setLastLoginIP(null); + User loginUser = userService.getLoginUser(); + if (loginUser == null || !id.equals(loginUser.getId())) { + // 未登录,并且获取的用户资料不是自己的,执行隐私控制过滤 + UserPrivacy privacy = privacyService.get(id); + List filters = privacy.listFilterFields(); + for (int i = 0; i < filters.size(); i++) { + Ref.setFieldValue(profile, filters.get(i), null); + } + } + return this; + } catch (Exception e) { + throw new TimiException(TimiCode.ERROR, "TODO filter user fail", e); + } + } +} diff --git a/src/main/java/com/imyeyu/server/modules/forevermc/bean/ServerStatus.java b/src/main/java/com/imyeyu/server/modules/forevermc/bean/ServerStatus.java new file mode 100644 index 0000000..ee88ac6 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/forevermc/bean/ServerStatus.java @@ -0,0 +1,158 @@ +package com.imyeyu.server.modules.forevermc.bean; + +import com.imyeyu.server.modules.forevermc.entity.Server; +import jakarta.validation.constraints.Min; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.ArrayList; +import java.util.List; + +/** + * 服务器状态报告请求 + * + * @author 夜雨 + * @since 2021-12-02 19:56 + */ +@Data +public class ServerStatus implements Cloneable { + + private String id; + + /** true 为维护中 */ + private boolean isDebugging; + + /** TPS */ + @Min(0) + private double tps; + + /** 报告时间 */ + private long reportAt; + + /** 静态基本信息 */ + private BaseInfo baseInfo; + + /** 主世界状态 */ + private List worldStatusList = new ArrayList<>(); + + /** JVM 状态 */ + private JVM jvm = new JVM(); + + /** 在线列表 */ + private List onlineList = new ArrayList<>(); + + @Override + protected ServerStatus clone() throws CloneNotSupportedException { + ServerStatus clone = (ServerStatus) super.clone(); + clone.id = id; + clone.isDebugging = isDebugging; + clone.tps = tps; + clone.reportAt = reportAt; + clone.baseInfo = baseInfo; + return clone; + } + + /** + * 静态基本信息 + * + * @author 夜雨 + * @since 2025-01-19 10:25 + */ + @Data + @EqualsAndHashCode(callSuper = true) + public static class BaseInfo extends Server implements Cloneable { + + /** 核心 */ + private String core; + + /** 启动时间 */ + private Long bootAt; + + /** 最大在线数量 */ + private Integer maxOnline; + + /** 图标 Base64 */ + private String icon; + + /** 游戏版本 */ + private String version; + + // @Override +// protected BaseInfo clone() throws CloneNotSupportedException { +// BaseInfo clone = (BaseInfo) super.clone(); +// clone.core = core; +// clone.bootAt = bootAt; +// } + + } + + /** + * JVM 状态 + * + * @author 夜雨 + * @since 2022-11-11 14:52 + */ + @Data + public static class JVM { + + /** JVM 名称 */ + private String name; + + /** CPU 已使用 */ + private double cpuUsed; + + /** 内存状态 */ + private Memory memory; + + /** + * 内存状态 + * + * @author 夜雨 + * @since 2022-11-15 09:54 + */ + @Data + public static class Memory { + + /** 已使用 */ + private long used; + + /** 已申请 */ + private long committed; + + /** 最大内存 */ + private long max; + } + } + + + /** + * 世界时间 + * + * @author 夜雨 + * @since 2024-10-29 23:59 + */ + @Data + public static class WorldStatus { + + /** 世界名称 */ + private String name; + + /** 总时刻 */ + private long ticks; + + /** 总天数 */ + private int day; + + /** 时 */ + private int hour; + + /** 分 */ + private int minute; + + /** true 为正在下雨 */ + private boolean isRaining; + + /** true 为正在雷雨 */ + private boolean isThundering; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/forevermc/controller/ServerController.java b/src/main/java/com/imyeyu/server/modules/forevermc/controller/ServerController.java new file mode 100644 index 0000000..a41a1bf --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/forevermc/controller/ServerController.java @@ -0,0 +1,115 @@ +package com.imyeyu.server.modules.forevermc.controller; + +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.forevermc.bean.ServerStatus; +import com.imyeyu.server.modules.forevermc.service.ServerService; +import com.imyeyu.server.modules.minecraft.annotation.RequiredFMCServerToken; +import com.imyeyu.server.modules.minecraft.vo.server.ReportRequest; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.IgnoreGlobalReturn; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.utils.Decoder; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.tika.Tika; +import org.springframework.beans.BeanUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * 服务器接口 + * + * @author 夜雨 + * @since 2024-08-06 17:20 + */ +@Slf4j +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/fmc/server") +public class ServerController { + + private final ServerService service; + + /** + * 服务器状态报告,由 FMCServer 服务端插件发送 + * + * @param request 服务器状态 + */ + @RequiredFMCServerToken + @PostMapping("/report") + public void report(@NotNull @RequestBody ReportRequest request) { + service.report(request); + } + @AOPLog + @RequestRateLimit + @IgnoreGlobalReturn + @GetMapping("/icon/{serverId}") + public void icon(@PathVariable String serverId, HttpServletResponse resp) { + try { + resp.setHeader("Pragma", "no-cache"); + resp.setHeader("Cache-Control", "no-cache"); + resp.setDateHeader("Expires", -1); + + byte[] bytes = Decoder.base64(service.getIcon(serverId)); + String mimeType = new Tika().detect(bytes); + if (TimiJava.isNotEmpty(mimeType)) { + resp.setContentType(mimeType); + } + resp.getOutputStream().write(bytes); + resp.getOutputStream().flush(); + resp.getOutputStream().close(); + } catch (TimiException e) { + if (e.getCode() == TimiCode.RESULT_NULL) { + resp.setStatus(HttpServletResponse.SC_NOT_FOUND); + resp.setCharacterEncoding(StandardCharsets.UTF_8.toString()); + } else { + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + resp.setCharacterEncoding(StandardCharsets.UTF_8.toString()); + } + } catch (Exception e) { + log.error("read attachment error", e); + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + resp.setCharacterEncoding(StandardCharsets.UTF_8.toString()); + } + } + + /** + * 获取服务器状态列表 + * + * @return 服务器状态列表 + */ + @RequestRateLimit + @GetMapping("/list") + public List listServer() { + List result = new ArrayList<>(); + List sourceList = service.listAll(); + for (int i = 0; i < sourceList.size(); i++) { + ServerStatus targetStatus = new ServerStatus(); + BeanUtils.copyProperties(sourceList.get(i), targetStatus); + if (targetStatus.getBaseInfo() != null) { + ServerStatus.BaseInfo targetBaseInfo = new ServerStatus.BaseInfo(); + BeanUtils.copyProperties(targetStatus.getBaseInfo(), targetBaseInfo); + targetBaseInfo.setIcon(null); + targetStatus.setBaseInfo(targetBaseInfo); + } + result.add(targetStatus); + } + result.sort(Comparator.comparingInt(o -> o.getBaseInfo().getOrder())); + return result; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/forevermc/entity/Server.java b/src/main/java/com/imyeyu/server/modules/forevermc/entity/Server.java new file mode 100644 index 0000000..c286ecd --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/forevermc/entity/Server.java @@ -0,0 +1,57 @@ +package com.imyeyu.server.modules.forevermc.entity; + +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.Destroyable; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * 服务器 + * + * @author 夜雨 + * @since 2025-01-27 20:36 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class Server extends UUIDEntity implements Destroyable { + + /** + * 类型 + * + * @author 夜雨 + * @since 2025-03-03 10:06 + */ + public enum Type { + + /** 官方原版 */ + ORIGINAL, + + /** 模组 */ + MOD + } + + /** 上级服务器 ID */ + protected String pid; + + /** 标题 */ + protected String title; + + /** 说明 */ + protected String description; + + /** 地址 */ + protected String host; + + /** 类型 */ + protected Type type; + + /** 排序 */ + protected int order; + + /** 客户端列表 */ + @Transient + protected List clientList; +} diff --git a/src/main/java/com/imyeyu/server/modules/forevermc/entity/ServerClient.java b/src/main/java/com/imyeyu/server/modules/forevermc/entity/ServerClient.java new file mode 100644 index 0000000..52a873b --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/forevermc/entity/ServerClient.java @@ -0,0 +1,40 @@ +package com.imyeyu.server.modules.forevermc.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.Entity; + +import java.util.List; + +/** + * 服务器客户端 + * + * @author 夜雨 + * @since 2025-01-27 20:40 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ServerClient extends Entity { + + /** 服务器 ID,关联 {@link Server#getId()} */ + private String serverId; + + /** 文件名 */ + private String fileName; + + /** 客户端版本 */ + private String version; + + /** 默认配置 */ + private String defOption; + + /** 文件大小 */ + private Long size; + + /** true 为已过时 */ + private boolean isDeprecated; + + @Transient + private List srcList; +} diff --git a/src/main/java/com/imyeyu/server/modules/forevermc/entity/ServerClientSrc.java b/src/main/java/com/imyeyu/server/modules/forevermc/entity/ServerClientSrc.java new file mode 100644 index 0000000..3e5609e --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/forevermc/entity/ServerClientSrc.java @@ -0,0 +1,52 @@ +package com.imyeyu.server.modules.forevermc.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.entity.Entity; + +/** + * 服务器客户端下载源 + * + * @author 夜雨 + * @since 2025-01-27 20:41 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ServerClientSrc extends Entity { + + /** + * 下载源类型 + * + * @author 夜雨 + * @since 2025-01-27 20:43 + */ + public enum BizType { + + /** Timi MongoDB */ + TIMI_MONGO, + + /** Timi 对象储存(未实现) */ + TIMI_COS, + + /** 第三方 URL */ + URL + } + + /** 客户端 ID,关联 {@link ServerClient#getId()} */ + private Long clientId; + + /** 服务器 ID,关联 {@link Server#getId()} */ + private String serverId; + + /** 下载源类型 */ + private BizType bizType; + + /** 下载源类型值 */ + private String bizValue; + + /** 排序 */ + private int order; + + /** true 为默认 */ + private boolean isDefault; +} diff --git a/src/main/java/com/imyeyu/server/modules/forevermc/mapper/ServerClientMapper.java b/src/main/java/com/imyeyu/server/modules/forevermc/mapper/ServerClientMapper.java new file mode 100644 index 0000000..52d00b4 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/forevermc/mapper/ServerClientMapper.java @@ -0,0 +1,17 @@ +package com.imyeyu.server.modules.forevermc.mapper; + +import com.imyeyu.server.modules.forevermc.entity.ServerClient; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2025-02-07 17:03 + */ +public interface ServerClientMapper extends BaseMapper { + + @Select("SELECT * FROM server_client WHERE server_id = #{serverId}" + NOT_DELETE) + List selectByServerId(String serverId); +} diff --git a/src/main/java/com/imyeyu/server/modules/forevermc/mapper/ServerClientSrcMapper.java b/src/main/java/com/imyeyu/server/modules/forevermc/mapper/ServerClientSrcMapper.java new file mode 100644 index 0000000..9cad8f1 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/forevermc/mapper/ServerClientSrcMapper.java @@ -0,0 +1,17 @@ +package com.imyeyu.server.modules.forevermc.mapper; + +import com.imyeyu.server.modules.forevermc.entity.ServerClientSrc; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2025-02-07 17:20 + */ +public interface ServerClientSrcMapper extends BaseMapper { + + @Select("SELECT * FROM server_client_src WHERE client_id = #{clientId}" + NOT_DELETE + " ORDER BY order ASC") + List selectByClientId(Long clientId); +} diff --git a/src/main/java/com/imyeyu/server/modules/forevermc/mapper/ServerMapper.java b/src/main/java/com/imyeyu/server/modules/forevermc/mapper/ServerMapper.java new file mode 100644 index 0000000..14293c7 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/forevermc/mapper/ServerMapper.java @@ -0,0 +1,11 @@ +package com.imyeyu.server.modules.forevermc.mapper; + +import com.imyeyu.server.modules.forevermc.entity.Server; +import com.imyeyu.spring.mapper.BaseMapper; + +/** + * @author 夜雨 + * @since 2025-01-27 20:44 + */ +public interface ServerMapper extends BaseMapper { +} diff --git a/src/main/java/com/imyeyu/server/modules/forevermc/service/ServerService.java b/src/main/java/com/imyeyu/server/modules/forevermc/service/ServerService.java new file mode 100644 index 0000000..1181d48 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/forevermc/service/ServerService.java @@ -0,0 +1,35 @@ +package com.imyeyu.server.modules.forevermc.service; + +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.forevermc.bean.ServerStatus; +import com.imyeyu.server.modules.minecraft.vo.server.ReportRequest; + +import java.util.List; + +/** + * Minecraft 服务 + * + * @author 夜雨 + * @since 2021-12-02 09:53 + */ +public interface ServerService extends TimiJava { + + /** + * 缓存服务器状态 + * + * @param request 服务器状态 + * @throws TimiException 服务异常 + */ + void report(ReportRequest request); + + String getIcon(String serverId); + + /** + * 获取所有服务器状态列表(此处移除与服务端的通信令牌) + * + * @return 所有服务器状态列表 + * @throws TimiException 服务异常 + */ + List listAll(); +} diff --git a/src/main/java/com/imyeyu/server/modules/forevermc/service/implement/ServerServiceImplement.java b/src/main/java/com/imyeyu/server/modules/forevermc/service/implement/ServerServiceImplement.java new file mode 100644 index 0000000..f5529b5 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/forevermc/service/implement/ServerServiceImplement.java @@ -0,0 +1,90 @@ +package com.imyeyu.server.modules.forevermc.service.implement; + +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.forevermc.bean.ServerStatus; +import com.imyeyu.server.modules.forevermc.entity.Server; +import com.imyeyu.server.modules.forevermc.entity.ServerClient; +import com.imyeyu.server.modules.forevermc.mapper.ServerClientMapper; +import com.imyeyu.server.modules.forevermc.mapper.ServerClientSrcMapper; +import com.imyeyu.server.modules.forevermc.mapper.ServerMapper; +import com.imyeyu.server.modules.forevermc.service.ServerService; +import com.imyeyu.server.modules.minecraft.vo.server.ReportRequest; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.BeanUtils; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Minecraft 服务实现 + * + * @author 夜雨 + * @version 2021-12-02 09:56 + */ +@Service +@EnableScheduling +@RequiredArgsConstructor +public class ServerServiceImplement implements ServerService { + + private final ServerMapper serverMapper; + private final ServerClientMapper serverClientMapper; + private final ServerClientSrcMapper serverClientSrcMapper; + + private final Map serverMap = new HashMap<>(); + + /** 周期性填充数据库的基本信息 */ + @Scheduled(fixedRate = Time.S * 30) + private void fillBaseInfo() { + serverMap.entrySet().removeIf(entry -> entry.getValue() == null); + for (Map.Entry item : serverMap.entrySet()) { + if (item.getValue().getBaseInfo() == null) { + continue; + } + Server server = serverMapper.select(item.getKey()); + TimiException.required(server, "not found server"); + + List clientList = serverClientMapper.selectByServerId(server.getId()); + for (int i = 0; i < clientList.size(); i++) { + ServerClient client = clientList.get(i); + client.setSrcList(serverClientSrcMapper.selectByClientId(client.getId())); + } + server.setClientList(clientList); + BeanUtils.copyProperties(server, item.getValue().getBaseInfo()); + } + } + + @Override + public void report(ReportRequest request) { + ServerStatus status = serverMap.get(request.getId()); + if (status != null) { + request.setBaseInfo(TimiJava.firstNotNull(request.getBaseInfo(), status.getBaseInfo())); + if (request.getBaseInfo() == null || TimiJava.isEmpty(request.getBaseInfo().getCore())) { + // 需要报告基本信息,与调用方约定返回代码为 IGNORE + throw new TimiException(TimiCode.IGNORE); + } + } + serverMap.put(request.getId(), request); + } + + @Override + public String getIcon(String serverId) { + ServerStatus status = serverMap.get(serverId); + if (status == null || status.getBaseInfo() == null || TimiJava.isEmpty(status.getBaseInfo().getIcon())) { + throw new TimiException(TimiCode.RESULT_NULL); + } + return status.getBaseInfo().getIcon(); + } + + @Override + public List listAll() { + return new ArrayList<>(serverMap.values()); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/git/bean/AttachType.java b/src/main/java/com/imyeyu/server/modules/git/bean/AttachType.java new file mode 100644 index 0000000..9f50bd8 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/bean/AttachType.java @@ -0,0 +1,14 @@ +package com.imyeyu.server.modules.git.bean; + +/** + * @author 夜雨 + * @since 2024-02-21 17:10 + */ +public enum AttachType { + + ISSUE, + + MERGE, + + RELEASE +} diff --git a/src/main/java/com/imyeyu/server/modules/git/bean/GitCommit.java b/src/main/java/com/imyeyu/server/modules/git/bean/GitCommit.java new file mode 100644 index 0000000..e8bcca5 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/bean/GitCommit.java @@ -0,0 +1,25 @@ +package com.imyeyu.server.modules.git.bean; + +import lombok.Data; +import com.imyeyu.server.modules.common.vo.user.UserView; + +/** + * Git 提交信息 + * + * @author 夜雨 + * @since 2023-08-14 17:13 + */ +@Data +public class GitCommit { + + /** ID */ + private String id; + + /** 说明 */ + private String msg; + + /** 时间 */ + private long time; + + private UserView committer; +} diff --git a/src/main/java/com/imyeyu/server/modules/git/bean/gitea/API.java b/src/main/java/com/imyeyu/server/modules/git/bean/gitea/API.java new file mode 100644 index 0000000..502e733 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/bean/gitea/API.java @@ -0,0 +1,59 @@ +package com.imyeyu.server.modules.git.bean.gitea; + +import com.imyeyu.java.TimiJava; +import com.imyeyu.server.TimiServerAPI; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.utils.Encoder; +import com.imyeyu.utils.StringInterpolator; + +import java.util.Map; + +/** + * @author 夜雨 + * @since 2025-06-26 16:04 + */ +public enum API { + + REPO_LIST("/api/v1/repos/search"), + + REPO_GET("/api/v1/repos/{owner}/{repoName}"), + + REPO_BRANCHES_LIST("/api/v1/repos/{owner}/{repoName}/branches"), + + REPO_FILE_LIST("/api/v1/repos/{owner}/{repoName}/contents/{path}"), + + REPO_FILE_RAW("/api/v1/repos/{owner}/{repoName}/raw/{path}"), + + REPO_ARCHIVE("/api/v1/repos/{owner}/{repoName}/archive/{archiveFormat}"), + + REPO_COMMIT_LIST("/api/v1/repos/{owner}/{repoName}/commits"), + + REPO_LANGUAGES("/api/v1/repos/{owner}/{repoName}/languages") + ; + + static final StringInterpolator INTERPOLATOR = new StringInterpolator(StringInterpolator.SIMPLE_OBJ); + + final String uri; + + API(String uri) { + this.uri = uri; + } + + public String buildURL(Map argsURI) { + return buildURL(argsURI, null); + + } + + public String buildURL(Map argsURI, Map argsURL) { + SettingService settingService = TimiServerAPI.applicationContext.getBean(SettingService.class); + StringBuilder url = new StringBuilder(); + url.append(settingService.getAsString(SettingKey.GIT_API)); + url.append(INTERPOLATOR.inject(uri, argsURI)); + if (TimiJava.isNotEmpty(argsURL)) { + url.append("?"); + url.append(Encoder.urlArgs(argsURL)); + } + return url.toString(); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/git/bean/gitea/Branch.java b/src/main/java/com/imyeyu/server/modules/git/bean/gitea/Branch.java new file mode 100644 index 0000000..bc1db81 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/bean/gitea/Branch.java @@ -0,0 +1,17 @@ +package com.imyeyu.server.modules.git.bean.gitea; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; + +/** + * @author 夜雨 + * @since 2025-06-28 00:31 + */ +@Data +public class Branch { + + private String name; + + @SerializedName("protected") + private boolean isProtected; +} diff --git a/src/main/java/com/imyeyu/server/modules/git/bean/gitea/File.java b/src/main/java/com/imyeyu/server/modules/git/bean/gitea/File.java new file mode 100644 index 0000000..9a6e1b1 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/bean/gitea/File.java @@ -0,0 +1,41 @@ +package com.imyeyu.server.modules.git.bean.gitea; + +import com.google.gson.annotations.JsonAdapter; +import com.imyeyu.server.modules.git.util.GiteaTimestampAdapter; +import lombok.Data; + +/** + * @author 夜雨 + * @since 2025-06-29 18:43 + */ +@Data +public class File { + + /** + * + * + * @author 夜雨 + * @since 2025-06-29 19:00 + */ + public enum Type { + + file, + + dir + } + + private String name; + + private String path; + + private String sha; + + private Long size; + + private Type type; + + private String lastCommitSha; + + @JsonAdapter(GiteaTimestampAdapter.class) + private Long lastCommitterDate; +} diff --git a/src/main/java/com/imyeyu/server/modules/git/bean/gitea/GiteaResponse.java b/src/main/java/com/imyeyu/server/modules/git/bean/gitea/GiteaResponse.java new file mode 100644 index 0000000..5955549 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/bean/gitea/GiteaResponse.java @@ -0,0 +1,14 @@ +package com.imyeyu.server.modules.git.bean.gitea; + +import lombok.Data; + +/** + * @author 夜雨 + * @since 2025-07-08 15:05 + */ +@Data +public class GiteaResponse { + + private boolean ok; + private T data; +} diff --git a/src/main/java/com/imyeyu/server/modules/git/bean/gitea/Repository.java b/src/main/java/com/imyeyu/server/modules/git/bean/gitea/Repository.java new file mode 100644 index 0000000..00eb1ca --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/bean/gitea/Repository.java @@ -0,0 +1,48 @@ +package com.imyeyu.server.modules.git.bean.gitea; + +import com.google.gson.annotations.JsonAdapter; +import com.imyeyu.server.modules.git.util.GiteaTimestampAdapter; +import lombok.Data; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2025-06-26 11:45 + */ +@Data +public class Repository { + + private Integer id; + + private String name; + + private String fullName; + + private String description; + + private Integer size; + + private String language; + + private String sshUrl; + + private String cloneUrl; + + private String avatarUrl; + + private String defaultBranch; + + private Boolean archived; + + @JsonAdapter(GiteaTimestampAdapter.class) + private Long createdAt; + + @JsonAdapter(GiteaTimestampAdapter.class) + private Long updatedAt; + + @JsonAdapter(GiteaTimestampAdapter.class) + private Long archivedAt; + + private List licenses; +} diff --git a/src/main/java/com/imyeyu/server/modules/git/bean/hook/PostReceive.java b/src/main/java/com/imyeyu/server/modules/git/bean/hook/PostReceive.java new file mode 100644 index 0000000..cb1f4c4 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/bean/hook/PostReceive.java @@ -0,0 +1,21 @@ +package com.imyeyu.server.modules.git.bean.hook; + +import lombok.Data; + +/** + * @author 夜雨 + * @since 2023-09-18 10:13 + */ +@Data +public class PostReceive { + + private String pusherName; + + private String repositoryName; + + private String branch; + + private String fromSHA1; + + private String toSHA1; +} diff --git a/src/main/java/com/imyeyu/server/modules/git/controller/DeveloperController.java b/src/main/java/com/imyeyu/server/modules/git/controller/DeveloperController.java new file mode 100644 index 0000000..e3ac23c --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/controller/DeveloperController.java @@ -0,0 +1,43 @@ +package com.imyeyu.server.modules.git.controller; + +import lombok.RequiredArgsConstructor; +import com.imyeyu.server.annotation.EnableSetting; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.git.entity.Developer; +import com.imyeyu.server.modules.git.service.DeveloperService; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.annotation.RequiredToken; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author 夜雨 + * @since 2023-09-19 13:48 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/git/developer") +public class DeveloperController { + + private final DeveloperService service; + + @AOPLog + @RequiredToken + @RequestRateLimit + @PostMapping("") + public Developer view() { + return service.view(); + } + + @AOPLog + @RequiredToken + @EnableSetting(value = SettingKey.ENABLE_USER_UPDATE, message = "user.data.off_service") + @RequestRateLimit + @PostMapping("/update") + public void update(@RequestBody Developer developer) { + service.update(developer); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/git/controller/IssueController.java b/src/main/java/com/imyeyu/server/modules/git/controller/IssueController.java new file mode 100644 index 0000000..17639ba --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/controller/IssueController.java @@ -0,0 +1,108 @@ +package com.imyeyu.server.modules.git.controller; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import com.imyeyu.java.TimiJava; +import com.imyeyu.server.annotation.CaptchaValid; +import com.imyeyu.server.bean.CaptchaFrom; +import com.imyeyu.server.modules.common.entity.Attachment; +import com.imyeyu.server.modules.common.service.AttachmentService; +import com.imyeyu.server.modules.common.service.CommentService; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.server.modules.git.entity.Issue; +import com.imyeyu.server.modules.git.service.IssueService; +import com.imyeyu.server.modules.git.vo.issue.IssuePage; +import com.imyeyu.server.modules.git.vo.issue.IssueRequest; +import com.imyeyu.server.modules.git.vo.issue.IssueView; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.annotation.RequiredToken; +import com.imyeyu.spring.bean.CaptchaData; +import com.imyeyu.spring.bean.PageResult; +import org.springframework.beans.BeanUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Git 反馈接口 + * + * @author 夜雨 + * @since 2023-08-15 15:02 + */ +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/git/issue") +public class IssueController { + + private final UserService userService; + private final IssueService service; + private final CommentService commentService; + private final AttachmentService attachmentService; + + /** + * 查看 + * + * @param id 反馈 ID + * @return 反馈数据 + */ + @AOPLog + @RequestRateLimit + @GetMapping("/{id}") + public IssueView view(@Min(1) @NotNull @PathVariable Long id) { + Issue issue = service.get(id); + IssueView view = new IssueView(); + BeanUtils.copyProperties(issue, view); + if (TimiJava.isNotEmpty(issue.getPublisherId())) { + view.setPublisher(userService.view(issue.getPublisherId())); + } + view.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GIT, id)); + return view; + } + + /** + * 获取反馈列表 + * + * @param issuePage 反馈页面参数 + * @return 反馈列表 + */ + @AOPLog + @RequestRateLimit + @PostMapping("/list") + public PageResult page(@Valid @RequestBody IssuePage issuePage) { + return service.page(issuePage); + } + + /** + * 创建反馈,不需要登录 + * + * @param captchaData 反馈请求 + */ + @AOPLog + @CaptchaValid(CaptchaFrom.GIT_ISSUE) + @RequestRateLimit + @PostMapping("/create") + public void create(@Valid @RequestBody CaptchaData captchaData) { + service.create(captchaData.getData()); + } + + /** + * 修改反馈,非讨论类型和未完成的反馈可以修改 + * + * @param issueRequest 修改反馈请求 + */ + @AOPLog + @RequiredToken + @RequestRateLimit + @PostMapping("/update") + public void update(@Valid @RequestBody IssueRequest issueRequest) { + service.update(issueRequest); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/git/controller/MergeController.java b/src/main/java/com/imyeyu/server/modules/git/controller/MergeController.java new file mode 100644 index 0000000..bd35772 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/controller/MergeController.java @@ -0,0 +1,110 @@ +package com.imyeyu.server.modules.git.controller; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import com.imyeyu.java.TimiJava; +import com.imyeyu.server.annotation.CaptchaValid; +import com.imyeyu.server.bean.CaptchaFrom; +import com.imyeyu.server.modules.common.entity.Attachment; +import com.imyeyu.server.modules.common.service.AttachmentService; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.server.modules.git.entity.Merge; +import com.imyeyu.server.modules.git.service.IssueService; +import com.imyeyu.server.modules.git.service.MergeService; +import com.imyeyu.server.modules.git.vo.merge.MergePage; +import com.imyeyu.server.modules.git.vo.merge.MergeRequest; +import com.imyeyu.server.modules.git.vo.merge.MergeView; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.annotation.RequiredToken; +import com.imyeyu.spring.bean.CaptchaData; +import com.imyeyu.spring.bean.PageResult; +import org.springframework.beans.BeanUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 和并请求接口 + * + * @author 夜雨 + * @since 2023-08-15 15:02 + */ +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/git/merge") +public class MergeController { + + private final UserService userService; + private final MergeService service; + private final IssueService issueService; + private final AttachmentService attachmentService; + + /** + * 合并请求详情 + * + * @param id 和并请求 ID + * @return 详情数据 + */ + @AOPLog + @RequestRateLimit + @GetMapping("/{id}") + public MergeView view(@Min(1) @NotNull @PathVariable Long id) { + Merge merge = service.get(id); + MergeView view = new MergeView(); + BeanUtils.copyProperties(merge, view); + if (TimiJava.isNotEmpty(view.getIssueId())) { + view.setIssue(issueService.get(view.getIssueId())); + } + view.setRequester(userService.view(view.getRequesterId())); + view.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GIT, id)); + return view; + } + + /** + * 获取合并请求列表 + * + * @param mergePage 页面参数 + * @return 列表 + */ + @AOPLog + @RequestRateLimit + @PostMapping("/list") + public PageResult page(@Valid @RequestBody MergePage mergePage) { + return service.page(mergePage); + } + + /** + * 申请合并请求 + * + * @param captchaData 申请数据 + */ + @AOPLog + @CaptchaValid(CaptchaFrom.GIT_MERGE) + @RequiredToken + @RequestRateLimit + @PostMapping("/create") + public void create(@Valid @RequestBody CaptchaData captchaData) { + service.create(captchaData.getData()); + } + + /** + * 修改合并请求 + * + * @param mergeRequest 修改数据 + */ + @AOPLog + @RequestRateLimit + @RequiredToken + @PostMapping("/update") + public void update(@Valid @RequestBody MergeRequest mergeRequest) { + service.update(mergeRequest); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/git/controller/ReleaseController.java b/src/main/java/com/imyeyu/server/modules/git/controller/ReleaseController.java new file mode 100644 index 0000000..3d2a0b0 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/controller/ReleaseController.java @@ -0,0 +1,41 @@ +package com.imyeyu.server.modules.git.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import com.imyeyu.server.modules.git.service.ReleaseService; +import com.imyeyu.server.modules.git.vo.release.ReleasePage; +import com.imyeyu.server.modules.git.vo.release.ReleaseView; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.bean.PageResult; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Git 项目版本发布接口 + * + * @author 夜雨 + * @since 2023-08-16 11:25 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/git/release") +public class ReleaseController { + + private final ReleaseService service; + + /** + * 获取版本发布列表 + * + * @param releasePage 版本发布页面参数 + * @return 版本发布列表 + */ + @AOPLog + @RequestRateLimit + @PostMapping("/list") + public PageResult page(@Valid @RequestBody ReleasePage releasePage) { + return service.pageByRepositoryId(releasePage); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/git/controller/RepositoryController.java b/src/main/java/com/imyeyu/server/modules/git/controller/RepositoryController.java new file mode 100644 index 0000000..1ea6c3e --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/controller/RepositoryController.java @@ -0,0 +1,166 @@ +package com.imyeyu.server.modules.git.controller; + +import com.imyeyu.io.IO; +import com.imyeyu.java.TimiJava; +import com.imyeyu.network.Network; +import com.imyeyu.server.modules.git.bean.gitea.File; +import com.imyeyu.server.modules.git.bean.gitea.Repository; +import com.imyeyu.server.modules.git.service.RepositoryService; +import com.imyeyu.server.modules.git.vo.repository.RepositoryView; +import com.imyeyu.server.modules.gitea.service.GiteaService; +import com.imyeyu.server.modules.gitea.vo.ActionLogView; +import com.imyeyu.spring.TimiSpring; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.IgnoreGlobalReturn; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.bean.Page; +import com.imyeyu.spring.bean.PageResult; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * Git 仓库接口 + * + * @author 夜雨 + * @since 2023-08-06 01:19 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/git/repository") +public class RepositoryController { + + private final GiteaService giteaService; + private final RepositoryService service; + + + @RequestRateLimit + @PostMapping("/log") + public PageResult pagePush(@Valid @RequestBody Page page) { + return giteaService.pagePush(page); + } + + /** + * 查看仓库 + * + * @param name 仓库 ID + * @return 仓库数据 + */ + @AOPLog + @RequestRateLimit + @GetMapping("/{name}") + public RepositoryView view(@PathVariable String name) { + Repository source = service.get(name); + RepositoryView view = new RepositoryView(); + BeanUtils.copyProperties(source, view); + view.setBranchList(service.listBranches(name)); + return view; + } + + /** + * 获取仓库列表 + * + * @param page 页面参数 + * @return 仓库列表 + */ + @RequestRateLimit + @PostMapping("/list") + public PageResult page(@Valid @RequestBody Page page) { + return service.page(page); + } + + /** + * 获取仓库文件结构树 + * + * @param repoName 仓库名称 + * @param branch 分支名称 + * @return 文件结构树 + */ + @AOPLog + @RequestRateLimit + @RequestMapping("/{repoName}:{branch}/file/list/**") + public List listFile(@PathVariable String repoName, @PathVariable String branch) { + String cutFlag = "/file/list/"; + String path = TimiSpring.getURI().substring(TimiSpring.getURI().indexOf(cutFlag) + cutFlag.length()); + return service.listFile(repoName, branch, path); + } + + /** + * 加载仓库文件,/request/raw/ 后的路径为加载目标文件的绝对仓库路径 + * + * @param repositoryName 仓库名称 + * @param branch 分支名称 + * @param resp 返回体 + */ + @RequestRateLimit + @IgnoreGlobalReturn + @GetMapping(value = "/{repositoryName}:{branch}/file/raw/**", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + public void raw(@PathVariable String repositoryName, @PathVariable String branch, HttpServletResponse resp) { + try { + String cutFlag = "/file/raw"; + String path = TimiSpring.getURI().substring(TimiSpring.getURI().indexOf(cutFlag) + cutFlag.length()); + if (TimiJava.isEmpty(path)) { + resp.setStatus(HttpServletResponse.SC_NOT_FOUND); + resp.setCharacterEncoding(StandardCharsets.UTF_8.toString()); + } else { + // TODO 获取 MIME_TYPE + IO.toOutputStream(service.getFileRaw(repositoryName, branch, path), resp.getOutputStream()); + } + } catch (Exception e) { + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + resp.setCharacterEncoding(StandardCharsets.UTF_8.toString()); + } + } + + /** + * 获取仓库提交消息列表 + * + * @param page 提交消息页面参数 + * @return 提交消息列表 + */ + @AOPLog + @RequestRateLimit + @PostMapping("/{repositoryName}:{branch}/log/push") + public PageResult pagePush(@PathVariable String repositoryName, @PathVariable String branch, @Valid @RequestBody Page page) { + if (branch.equals("all")) { + branch = null; + } + return giteaService.pagePush(repositoryName, branch, page); + } + + /** + * 打包下载仓库源码文件 + * + * @param repositoryName 仓库名称 + * @param branch 分支名称 + * @param resp 返回体 + */ + @AOPLog + @RequestRateLimit + @RequestMapping("/{repositoryName}:{branch}/archive") + public void downloadArchive(@PathVariable String repositoryName, @PathVariable String branch, HttpServletResponse resp) { + try { + resp.setHeader("Content-Disposition", Network.getFileDownloadHeader("%s-%s.tar.gz".formatted(repositoryName.toLowerCase(), branch))); + resp.setHeader("Accept-Ranges", "bytes"); + IO.toOutputStream(service.getArchive(repositoryName, branch), resp.getOutputStream()); + resp.flushBuffer(); + } catch (Exception e) { + log.error("downloadArchive error", e); + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + resp.setCharacterEncoding(StandardCharsets.UTF_8.toString()); + } + } +} diff --git a/src/main/java/com/imyeyu/server/modules/git/entity/CommitLog.java b/src/main/java/com/imyeyu/server/modules/git/entity/CommitLog.java new file mode 100644 index 0000000..dc597dc --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/entity/CommitLog.java @@ -0,0 +1,33 @@ +package com.imyeyu.server.modules.git.entity; + +import com.imyeyu.spring.annotation.table.Id; +import com.imyeyu.spring.annotation.table.Table; +import com.imyeyu.spring.entity.Creatable; +import com.imyeyu.spring.entity.IDEntity; +import lombok.Data; + +/** + * 提交日志 + * + * @author 夜雨 + * @since 2023-09-19 11:34 + */ +@Data +@Table("git_commit_log") +public class CommitLog implements IDEntity, Creatable { + + @Id + protected Long id; + + /** 所属推送 ID */ + protected Long pushId; + + /** SHA1 */ + protected String sha1; + + /** 说明 */ + protected String message; + + /** 提交时间 */ + protected Long createdAt; +} diff --git a/src/main/java/com/imyeyu/server/modules/git/entity/Developer.java b/src/main/java/com/imyeyu/server/modules/git/entity/Developer.java new file mode 100644 index 0000000..be65188 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/entity/Developer.java @@ -0,0 +1,37 @@ +package com.imyeyu.server.modules.git.entity; + +import com.imyeyu.server.modules.common.entity.User; +import com.imyeyu.spring.annotation.table.Id; +import com.imyeyu.spring.annotation.table.Table; +import com.imyeyu.spring.entity.Updatable; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 开发者 + * + * @author 夜雨 + * @since 2023-09-19 11:37 + */ +@Data +@NoArgsConstructor +@Table("git_developer") +public class Developer implements Updatable { + + /** 开发者 ID({@link User#getId()}) */ + @Id + protected Long developerId; + + /** 昵称 */ + protected String name; + + /** RSA 密钥 */ + protected String rsa; + + /** 更新时间 */ + protected Long updatedAt; + + public Developer(Long developerId) { + this.developerId = developerId; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/git/entity/Issue.java b/src/main/java/com/imyeyu/server/modules/git/entity/Issue.java new file mode 100644 index 0000000..2ad0718 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/entity/Issue.java @@ -0,0 +1,120 @@ +package com.imyeyu.server.modules.git.entity; + +import com.imyeyu.spring.annotation.table.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.server.modules.common.bean.CommentSupport; +import com.imyeyu.spring.entity.Entity; + +/** + * Git 问题反馈 + * + * @author 夜雨 + * @since 2023-08-16 15:18 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table("git_issue") +public class Issue extends Entity implements CommentSupport { + + /** + * 类型 + * + * @author 夜雨 + * @since 2023-08-16 17:18 + */ + public enum Type { + + /** 异常 */ + BUG, + + /** 功能 */ + FEATURE, + + /** 安全 */ + SECURITY, + + /** 提问 */ + QUESTION, + + /** 讨论 */ + DISCUSS + } + + /** + * 状态 + * + * @author 夜雨 + * @since 2023-08-16 15:26 + */ + public enum Status { + + /** 待确认 */ + BEFORE_CONFIRM, + + /** 已确认,待开发 */ + CONFIRMED, + + /** 开发中 */ + DEVELOPING, + + /** 已完成 */ + FINISHED, + + /** 已关闭 */ + CLOSED + } + + /** 所属仓库 ID */ + protected long repositoryId; + + /** 发布者 ID */ + protected Long publisherId; + + /** 发布者昵称 */ + protected String publisherNick; + + /** 类型 */ + protected Type type; + + /** 版本 */ + protected String version; + + /** 标题 */ + protected String title; + + /** 描述 */ + protected String description; + + /** 状态 */ + protected Status status; + + /** 确认时间 */ + protected Long confirmedAt; + + /** 开发时间 */ + protected Long developAt; + + /** 关闭时间 */ + protected Long closedAt; + + @Override + public boolean canComment() { + return status != Status.CLOSED; + } + + @Override + public boolean canNotComment() { + return !canComment(); + } + + /** @return true 为可更新 */ + public boolean canUpdate() { + return type != Type.DISCUSS && !isClosed() && status != Status.FINISHED; + } + + /** @return true 为已关闭 */ + public boolean isClosed() { + return closedAt != null; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/git/entity/Merge.java b/src/main/java/com/imyeyu/server/modules/git/entity/Merge.java new file mode 100644 index 0000000..b8756dc --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/entity/Merge.java @@ -0,0 +1,133 @@ +package com.imyeyu.server.modules.git.entity; + +import com.imyeyu.spring.annotation.table.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.server.modules.common.bean.CommentSupport; +import com.imyeyu.spring.entity.Entity; + +/** + * 合并请求 + * + * @author 夜雨 + * @since 2023-08-16 14:52 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table("git_merge") +public class Merge extends Entity implements CommentSupport { + + /** + * 类型 + * + * @author 夜雨 + * @since 2023-08-17 11:32 + */ + public enum Type { + + /** 异常 */ + BUG, + + /** 功能 */ + FEATURE, + + /** 安全 */ + SECURITY, + + /** 文档改进 */ + DOCUMENT, + + /** 重构 */ + REFACTOR + } + + /** + * 状态 + * + * @author 夜雨 + * @since 2023-08-17 11:33 + */ + public enum Status { + + /** 等待代码审查 */ + BEFORE_CHECK, + + /** 等待合并 */ + WAITING, + + /** 已合并 */ + MERGED, + + /** 已拒绝 */ + REJECTED, + + CLOSED + } + + /** 所属仓库 ID */ + protected long repositoryId; + + /** 申请合并用户 ID */ + protected long requesterId; + + /** 相关反馈 ID */ + protected Long issueId; + + /** 类型 */ + protected Type type; + + /** 来自分支 */ + protected String fromBranch; + + /** 去向分支(基准分支) */ + protected String toBranch; + + /** 说明标题 */ + protected String title; + + /** 合并说明 */ + protected String description; + + /** 通过审查时间 */ + protected Long checkedAt; + + /** 合并时间 */ + protected Long mergedAt; + + /** 拒绝时间 */ + protected Long rejectedAt; + + /** 拒绝原因 */ + protected String rejectReason; + + /** 关闭时间 */ + protected Long closedAt; + + /** 请求状态 */ + protected Status status; + + @Override + public boolean canComment() { + return status != Status.CLOSED; + } + + @Override + public boolean canNotComment() { + return !canComment(); + } + + /** @return true 为可更新 */ + public boolean canUpdate() { + return status == Status.BEFORE_CHECK && !isClosed(); + } + + /** @return true 为可更新 */ + public boolean canNotUpdate() { + return !canUpdate(); + } + + /** @return true 为已关闭 */ + public boolean isClosed() { + return closedAt != null; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/git/entity/PushLog.java b/src/main/java/com/imyeyu/server/modules/git/entity/PushLog.java new file mode 100644 index 0000000..a47062c --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/entity/PushLog.java @@ -0,0 +1,39 @@ +package com.imyeyu.server.modules.git.entity; + +import com.imyeyu.spring.annotation.table.Id; +import com.imyeyu.spring.annotation.table.Table; +import com.imyeyu.spring.entity.Creatable; +import com.imyeyu.spring.entity.IDEntity; +import lombok.Data; + +/** + * Git 推送日志 + * + * @author 夜雨 + * @since 2023-09-19 11:35 + */ +@Data +@Table("git_push_log") +public class PushLog implements IDEntity, Creatable { + + @Id + protected Long id; + + /** 所属仓库 ID */ + protected Long repositoryId; + + /** 推送 ID */ + protected Long pusherId; + + /** 分支 */ + protected String branch; + + /** 来源提交 */ + protected String from; + + /** 去向提交 */ + protected String to; + + /** 推送时间 */ + protected Long createdAt; +} diff --git a/src/main/java/com/imyeyu/server/modules/git/entity/Release.java b/src/main/java/com/imyeyu/server/modules/git/entity/Release.java new file mode 100644 index 0000000..d0381a0 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/entity/Release.java @@ -0,0 +1,33 @@ +package com.imyeyu.server.modules.git.entity; + +import com.imyeyu.spring.annotation.table.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.entity.Entity; + +/** + * Git 版本发布 + * + * @author 夜雨 + * @since 2023-08-16 11:25 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table("git_release") +public class Release extends Entity { + + /** 所属仓库 ID */ + protected long repositoryId; + + /** 版本 */ + protected String version; + + /** 描述 */ + protected String description; + + /** SHA1 */ + protected String sha1; + + /** 发布时提交数量 */ + protected int commits; +} diff --git a/src/main/java/com/imyeyu/server/modules/git/mapper/DeveloperMapper.java b/src/main/java/com/imyeyu/server/modules/git/mapper/DeveloperMapper.java new file mode 100644 index 0000000..4669d7c --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/mapper/DeveloperMapper.java @@ -0,0 +1,15 @@ +package com.imyeyu.server.modules.git.mapper; + +import com.imyeyu.server.modules.git.entity.Developer; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; + +/** + * @author 夜雨 + * @since 2023-09-19 11:42 + */ +public interface DeveloperMapper extends BaseMapper { + + @Select("SELECT * FROM git_developer WHERE BINARY name = #{name} LIMIT 1") + Developer queryByName(String name); +} diff --git a/src/main/java/com/imyeyu/server/modules/git/mapper/IssueMapper.java b/src/main/java/com/imyeyu/server/modules/git/mapper/IssueMapper.java new file mode 100644 index 0000000..64319ae --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/mapper/IssueMapper.java @@ -0,0 +1,35 @@ +package com.imyeyu.server.modules.git.mapper; + +import com.imyeyu.server.modules.git.entity.Issue; +import com.imyeyu.server.modules.git.vo.issue.IssuePage; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * Git 反馈表 + * + * @author 夜雨 + * @since 2023-08-16 15:19 + */ +public interface IssueMapper extends BaseMapper { + + /** + * 根据仓库 ID 统计总反馈数量 + * + * @param issuePage 仓库 ID + * @return 仓库反馈总数量 + */ + long countByIssuePage(@Param("issuePage") IssuePage issuePage); + + /** + * 根据仓库 ID 查询部分反馈 + * + * @param issuePage 仓库 ID + * @param offset 偏移 + * @param limit 数据量 + * @return 反馈列表 + */ + List listByIssuePage(@Param("issuePage") IssuePage issuePage, long offset, int limit); +} diff --git a/src/main/java/com/imyeyu/server/modules/git/mapper/MergeMapper.java b/src/main/java/com/imyeyu/server/modules/git/mapper/MergeMapper.java new file mode 100644 index 0000000..6cdb53a --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/mapper/MergeMapper.java @@ -0,0 +1,23 @@ +package com.imyeyu.server.modules.git.mapper; + +import com.imyeyu.server.modules.git.entity.Merge; +import com.imyeyu.server.modules.git.vo.merge.MergePage; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2023-08-17 11:42 + */ +public interface MergeMapper extends BaseMapper { + + @Select("SELECT * FROM git_merge WHERE id = #{id}" + NOT_DELETE + LIMIT_1) + Merge select(Long id); + + long countByMergePage(@Param("mergePage") MergePage mergePage); + + List listByMergePage(@Param("mergePage") MergePage mergePage, long offset, int limit); +} diff --git a/src/main/java/com/imyeyu/server/modules/git/mapper/ReleaseMapper.java b/src/main/java/com/imyeyu/server/modules/git/mapper/ReleaseMapper.java new file mode 100644 index 0000000..4867150 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/mapper/ReleaseMapper.java @@ -0,0 +1,23 @@ +package com.imyeyu.server.modules.git.mapper; + +import com.imyeyu.server.modules.git.entity.Release; +import com.imyeyu.server.modules.git.vo.release.ReleaseView; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2023-08-16 11:30 + */ +public interface ReleaseMapper extends BaseMapper { + + @Select("SELECT COUNT(1) FROM git_release WHERE repository_id = #{repositoryId}" + NOT_DELETE) + long countByRepositoryId(long repositoryId); + + List listByRepositoryId(long repositoryId, long offset, int limit); + + @Select("SELECT * FROM git_release WHERE repository_id = #{repositoryId}" + NOT_DELETE + "ORDER BY created_at DESC" + LIMIT_1) + Release queryLatestByRepositoryId(long repositoryId); +} diff --git a/src/main/java/com/imyeyu/server/modules/git/service/DeveloperService.java b/src/main/java/com/imyeyu/server/modules/git/service/DeveloperService.java new file mode 100644 index 0000000..f572cad --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/service/DeveloperService.java @@ -0,0 +1,17 @@ +package com.imyeyu.server.modules.git.service; + +import com.imyeyu.server.modules.git.entity.Developer; +import com.imyeyu.spring.service.CreatableService; +import com.imyeyu.spring.service.GettableService; +import com.imyeyu.spring.service.UpdatableService; + +/** + * @author 夜雨 + * @since 2023-09-19 11:40 + */ +public interface DeveloperService extends GettableService, CreatableService, UpdatableService { + + Developer view(); + + Developer getByName(String name); +} diff --git a/src/main/java/com/imyeyu/server/modules/git/service/IssueService.java b/src/main/java/com/imyeyu/server/modules/git/service/IssueService.java new file mode 100644 index 0000000..07423ca --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/service/IssueService.java @@ -0,0 +1,20 @@ +package com.imyeyu.server.modules.git.service; + +import com.imyeyu.server.modules.git.entity.Issue; +import com.imyeyu.server.modules.git.vo.issue.IssuePage; +import com.imyeyu.server.modules.git.vo.issue.IssueRequest; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.GettableService; + +/** + * @author 夜雨 + * @since 2023-08-16 15:18 + */ +public interface IssueService extends GettableService { + + void create(IssueRequest issueRequest); + + void update(IssueRequest issueRequest); + + PageResult page(IssuePage issuePage); +} diff --git a/src/main/java/com/imyeyu/server/modules/git/service/MergeService.java b/src/main/java/com/imyeyu/server/modules/git/service/MergeService.java new file mode 100644 index 0000000..a132405 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/service/MergeService.java @@ -0,0 +1,20 @@ +package com.imyeyu.server.modules.git.service; + +import com.imyeyu.server.modules.git.entity.Merge; +import com.imyeyu.server.modules.git.vo.merge.MergePage; +import com.imyeyu.server.modules.git.vo.merge.MergeRequest; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.GettableService; + +/** + * @author 夜雨 + * @since 2023-08-16 14:46 + */ +public interface MergeService extends GettableService { + + void create(MergeRequest mergeRequest); + + void update(MergeRequest mergeRequest); + + PageResult page(MergePage mergePage); +} diff --git a/src/main/java/com/imyeyu/server/modules/git/service/ReleaseService.java b/src/main/java/com/imyeyu/server/modules/git/service/ReleaseService.java new file mode 100644 index 0000000..8c94d83 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/service/ReleaseService.java @@ -0,0 +1,14 @@ +package com.imyeyu.server.modules.git.service; + +import com.imyeyu.server.modules.git.vo.release.ReleasePage; +import com.imyeyu.server.modules.git.vo.release.ReleaseView; +import com.imyeyu.spring.bean.PageResult; + +/** + * @author 夜雨 + * @since 2023-08-16 11:27 + */ +public interface ReleaseService { + + PageResult pageByRepositoryId(ReleasePage releasePage); +} diff --git a/src/main/java/com/imyeyu/server/modules/git/service/RepositoryService.java b/src/main/java/com/imyeyu/server/modules/git/service/RepositoryService.java new file mode 100644 index 0000000..c9bfc62 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/service/RepositoryService.java @@ -0,0 +1,42 @@ +package com.imyeyu.server.modules.git.service; + +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.git.bean.gitea.Branch; +import com.imyeyu.server.modules.git.bean.gitea.File; +import com.imyeyu.server.modules.git.bean.gitea.Repository; +import com.imyeyu.spring.service.PageableService; + +import java.io.InputStream; +import java.util.List; + +/** + * Git 仓库服务 + * + * @author 夜雨 + * @since 2023-08-06 01:19 + */ +public interface RepositoryService extends PageableService { + + /** + * 根据名称获取数据库仓库 + * + * @param repoName 仓库名 + * @return 数据库仓库 + * @throws TimiException 服务异常 + */ + Repository get(String repoName); + /** + * 获取仓库所有分支列表 + * + * @param repoName 仓库名 + * @return 分支列表 + * @throws TimiException 服务异常 + */ + List listBranches(String repoName); + + List listFile(String repoName, String branch, String path); + + InputStream getFileRaw(String repoName, String branch, String path); + + InputStream getArchive(String repoName, String branch); +} diff --git a/src/main/java/com/imyeyu/server/modules/git/service/implement/DeveloperServiceImplement.java b/src/main/java/com/imyeyu/server/modules/git/service/implement/DeveloperServiceImplement.java new file mode 100644 index 0000000..e4bdf60 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/service/implement/DeveloperServiceImplement.java @@ -0,0 +1,66 @@ +package com.imyeyu.server.modules.git.service.implement; + +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.config.dbsource.TimiServerDBConfig; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.server.modules.git.entity.Developer; +import com.imyeyu.server.modules.git.mapper.DeveloperMapper; +import com.imyeyu.server.modules.git.service.DeveloperService; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author 夜雨 + * @since 2023-09-19 11:41 + */ +@Slf4j +@Service +@RequiredArgsConstructor(onConstructor_ = { @Lazy}) +public class DeveloperServiceImplement extends AbstractEntityService implements DeveloperService { + + private final UserService userService; + + private final DeveloperMapper mapper; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void update(Developer developer) { + Developer dbDeveloper = get(userService.getLoginUser().getId()); + dbDeveloper.setName(developer.getName()); + dbDeveloper.setRsa(developer.getRsa()); + + if (TimiJava.isNotEmpty(dbDeveloper.getName())) { + Developer developerByName = getByName(dbDeveloper.getName()); + if (developerByName != null && !developerByName.getDeveloperId().equals(dbDeveloper.getDeveloperId())) { + throw new TimiException(TimiCode.ARG_BAD).msgKey("TODO 开发者冲突,此名称已被使用"); + } + } + if (TimiJava.isNotEmpty(dbDeveloper.getRsa())) { + if (!dbDeveloper.getRsa().startsWith("ssh-rsa")) { + throw new TimiException(TimiCode.ARG_BAD).msgKey("TODO 无效的 RSA 公钥"); + } + } + super.update(dbDeveloper); + } + + @Override + public Developer view() { + return get(userService.getLoginUser().getId()); + } + + public Developer getByName(String name) { + return mapper.queryByName(name); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/git/service/implement/IssueServiceImplement.java b/src/main/java/com/imyeyu/server/modules/git/service/implement/IssueServiceImplement.java new file mode 100644 index 0000000..0dae219 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/service/implement/IssueServiceImplement.java @@ -0,0 +1,97 @@ +package com.imyeyu.server.modules.git.service.implement; + +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.config.dbsource.TimiServerDBConfig; +import com.imyeyu.server.modules.common.entity.User; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.server.modules.git.entity.Issue; +import com.imyeyu.server.modules.git.mapper.IssueMapper; +import com.imyeyu.server.modules.git.service.IssueService; +import com.imyeyu.server.modules.git.vo.issue.IssuePage; +import com.imyeyu.server.modules.git.vo.issue.IssueRequest; +import com.imyeyu.server.modules.gitea.entity.Repository; +import com.imyeyu.server.modules.gitea.service.GiteaService; +import com.imyeyu.spring.TimiSpring; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Git 反馈服务 + * + * @author 夜雨 + * @since 2023-08-16 15:18 + */ +@Service +@RequiredArgsConstructor +public class IssueServiceImplement extends AbstractEntityService implements IssueService { + + private final UserService userService; + private final GiteaService giteaService; + + private final IssueMapper mapper; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void create(IssueRequest issueRequest) { + Repository repository = giteaService.getRepository(issueRequest.getRepositoryId()); + Issue issue = new Issue(); + // 令牌和账号验证 + if (TimiJava.isNotEmpty(TimiSpring.getToken())) { + User publisher = userService.getLoginUser(); + TimiException.requiredTrue(!publisher.isBanning() && !publisher.isMuting(), "TODO publisher.banned"); + issue.setPublisherId(publisher.getId()); + issue.setPublisherNick(null); + } else { + // 昵称 + TimiException.required(issueRequest.getPublisherNick(), "TODO comment.nick.empty"); + issue.setPublisherNick(issueRequest.getPublisherNick()); + } + TimiException.required(issueRequest.getTitle(), "TODO comment.title.empty"); + TimiException.required(issueRequest.getDescription(), "TODO comment.data.empty"); + issue.setRepositoryId(repository.getId()); + issue.setType(issueRequest.getType()); + issue.setVersion(issueRequest.getVersion()); + issue.setTitle(issueRequest.getTitle()); + issue.setDescription(issueRequest.getDescription()); + issue.setStatus(Issue.Status.BEFORE_CONFIRM); + super.create(issue); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void update(IssueRequest issueRequest) { + Issue issue = get(issueRequest.getId()); + TimiException.requiredTrue(issue.canUpdate(), "TODO can not update"); + + User publisher = userService.getLoginUser(); + TimiException.required(publisher.getId().equals(issue.getPublisherId()), "TODO not permission edit issue"); + TimiException.requiredTrue(!publisher.isMuting(), "TODO publisher.banned"); + + TimiException.required(issueRequest.getTitle(), "TODO comment.title.empty"); + TimiException.required(issueRequest.getDescription(), "TODO comment.data.empty"); + issue.setType(issueRequest.getType()); + issue.setVersion(issueRequest.getVersion()); + issue.setTitle(issueRequest.getTitle()); + issue.setDescription(issueRequest.getDescription()); + mapper.update(issue); + } + + @Override + public PageResult page(IssuePage issuePage) { + PageResult result = new PageResult<>(); + result.setList(mapper.listByIssuePage(issuePage, issuePage.getOffset(), issuePage.getLimit())); + result.setTotal(mapper.countByIssuePage(issuePage)); + return result; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/git/service/implement/MergeServiceImplement.java b/src/main/java/com/imyeyu/server/modules/git/service/implement/MergeServiceImplement.java new file mode 100644 index 0000000..f718e52 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/service/implement/MergeServiceImplement.java @@ -0,0 +1,112 @@ +package com.imyeyu.server.modules.git.service.implement; + +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.config.dbsource.TimiServerDBConfig; +import com.imyeyu.server.modules.common.entity.User; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.server.modules.git.entity.Issue; +import com.imyeyu.server.modules.git.entity.Merge; +import com.imyeyu.server.modules.git.mapper.MergeMapper; +import com.imyeyu.server.modules.git.service.IssueService; +import com.imyeyu.server.modules.git.service.MergeService; +import com.imyeyu.server.modules.git.service.RepositoryService; +import com.imyeyu.server.modules.git.vo.merge.MergePage; +import com.imyeyu.server.modules.git.vo.merge.MergeRequest; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author 夜雨 + * @since 2023-08-17 11:38 + */ +@Service +@RequiredArgsConstructor +public class MergeServiceImplement extends AbstractEntityService implements MergeService { + + private final UserService userService; + private final IssueService issueService; + private final RepositoryService repositoryService; + + private final MergeMapper mapper; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void create(MergeRequest mergeRequest) { +// Repository repository = repositoryService.get(mergeRequest.getRepositoryId()); + User requester = userService.getLoginUser(); + TimiException.required(mergeRequest.getTitle(), "TODO mergeRequest.title.empty"); + TimiException.required(mergeRequest.getDescription(), "TODO mergeRequest.description.empty"); + TimiException.required(mergeRequest.getFromBranch(), "TODO mergeRequest.fromBranch.empty"); +// List branches = repositoryService.listAllBranches(repository.getId()); +// if (branches.stream().filter(i -> i.getName().equals(mergeRequest.getFromBranch())).toList().isEmpty()) { +// throw new TimiException(TimiCode.ARG_MISS).msgKey("TODO 来源分支不存在"); +// } +// if (branches.stream().filter(i -> i.getName().equals(mergeRequest.getToBranch())).toList().isEmpty()) { +// throw new TimiException(TimiCode.ARG_MISS).msgKey("TODO 去向分支不存在"); +// } + Merge merge = new Merge(); + if (TimiJava.isNotEmpty(mergeRequest.getIssueId())) { + Issue issue = issueService.get(mergeRequest.getIssueId()); + merge.setIssueId(issue.getId()); + } +// merge.setRepositoryId(repository.getId()); + merge.setRequesterId(requester.getId()); + merge.setType(mergeRequest.getType()); + merge.setFromBranch(mergeRequest.getFromBranch()); + merge.setToBranch(mergeRequest.getToBranch()); + merge.setTitle(mergeRequest.getTitle()); + merge.setDescription(mergeRequest.getDescription()); + merge.setStatus(Merge.Status.BEFORE_CHECK); + mapper.insert(merge); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void update(MergeRequest mergeRequest) { + Merge merge = get(mergeRequest.getId()); + + TimiException.requiredTrue(merge.canNotUpdate(), "TODO can not update"); + TimiException.required(mergeRequest.getTitle(), "TODO mergeRequest.title.empty"); + TimiException.required(mergeRequest.getDescription(), "TODO mergeRequest.data.empty"); + TimiException.requiredTrue(mergeRequest.getFromBranch().equals(merge.getToBranch()), "TODO 来源分支和去向分支不能相等"); + + User requester = userService.getLoginUser(); + TimiException.requiredTrue(!requester.getId().equals(merge.getRequesterId()), "TODO not permission edit merge"); + +// List branches = repositoryService.listAllBranches(merge.getRepositoryId()); +// if (branches.stream().filter(i -> i.getName().equals(mergeRequest.getFromBranch())).toList().isEmpty()) { +// throw new TimiException(TimiCode.ARG_MISS).msgKey("TODO 来源分支不存在"); +// } +// if (branches.stream().filter(i -> i.getName().equals(mergeRequest.getToBranch())).toList().isEmpty()) { +// throw new TimiException(TimiCode.ARG_MISS).msgKey("TODO 去向分支不存在"); +// } +// if (TimiJava.isNotEmpty(mergeRequest.getIssueId())) { +// Issue issue = issueService.get(mergeRequest.getIssueId()); +// merge.setIssueId(issue.getId()); +// } + merge.setType(mergeRequest.getType()); + merge.setFromBranch(mergeRequest.getFromBranch()); + merge.setToBranch(mergeRequest.getToBranch()); + merge.setTitle(mergeRequest.getTitle()); + merge.setDescription(mergeRequest.getDescription()); + mapper.update(merge); + } + + @Override + public PageResult page(MergePage mergePage) { + PageResult result = new PageResult<>(); + result.setList(mapper.listByMergePage(mergePage, mergePage.getOffset(), mergePage.getLimit())); + result.setTotal(mapper.countByMergePage(mergePage)); + return result; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/git/service/implement/ReleaseServiceImplement.java b/src/main/java/com/imyeyu/server/modules/git/service/implement/ReleaseServiceImplement.java new file mode 100644 index 0000000..3c8c721 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/service/implement/ReleaseServiceImplement.java @@ -0,0 +1,32 @@ +package com.imyeyu.server.modules.git.service.implement; + +import lombok.RequiredArgsConstructor; +import com.imyeyu.server.modules.git.mapper.ReleaseMapper; +import com.imyeyu.server.modules.git.service.ReleaseService; +import com.imyeyu.server.modules.git.service.RepositoryService; +import com.imyeyu.server.modules.git.vo.release.ReleasePage; +import com.imyeyu.server.modules.git.vo.release.ReleaseView; +import com.imyeyu.spring.bean.PageResult; +import org.springframework.stereotype.Service; + +/** + * @author 夜雨 + * @since 2023-08-16 11:27 + */ +@Service +@RequiredArgsConstructor +public class ReleaseServiceImplement implements ReleaseService { + + private final RepositoryService repositoryService; + + private final ReleaseMapper mapper; + + @Override + public PageResult pageByRepositoryId(ReleasePage releasePage) { +// Repository repository = repositoryService.get(releasePage.getRepositoryId()); + PageResult result = new PageResult<>(); +// result.setList(mapper.listByRepositoryId(repository.getId(), releasePage.getOffset(), releasePage.getLimit())); +// result.setTotal(mapper.countByRepositoryId(repository.getId())); + return result; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/git/service/implement/RepositoryServiceImplement.java b/src/main/java/com/imyeyu/server/modules/git/service/implement/RepositoryServiceImplement.java new file mode 100644 index 0000000..da67796 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/service/implement/RepositoryServiceImplement.java @@ -0,0 +1,169 @@ +package com.imyeyu.server.modules.git.service.implement; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.server.modules.git.bean.gitea.API; +import com.imyeyu.server.modules.git.bean.gitea.Branch; +import com.imyeyu.server.modules.git.bean.gitea.File; +import com.imyeyu.server.modules.git.bean.gitea.GiteaResponse; +import com.imyeyu.server.modules.git.bean.gitea.Repository; +import com.imyeyu.server.modules.git.service.RepositoryService; +import com.imyeyu.server.modules.gitea.entity.User; +import com.imyeyu.server.modules.gitea.service.GiteaService; +import com.imyeyu.spring.bean.Page; +import com.imyeyu.spring.bean.PageResult; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.client5.http.fluent.Request; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.springframework.stereotype.Service; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; + +/** + * Git 仓库服务 + * + * @author 夜雨 + * @since 2023-08-06 01:19 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class RepositoryServiceImplement implements RepositoryService { + + private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + + private final UserService userService; + private final GiteaService giteaService; + private final SettingService settingService; + + private User owner; + + @PostConstruct + private void postConstruct() { + owner = giteaService.getOwner(); + } + + @Override + public PageResult page(Page page) { + try { + ClassicHttpResponse resp = (ClassicHttpResponse) Request.get(API.REPO_LIST.buildURL(null, new HashMap<>() {{ + put("uid", owner.getId()); + put("sort", "updated"); + put("order", "desc"); + put("page", page.getIndex() + 1); + put("limit", page.getLimit()); + }})).execute().returnResponse(); + String respText = EntityUtils.toString(resp.getEntity()); + GiteaResponse> respObj = gson.fromJson(respText, new TypeToken>>() { + }.getType()); + + PageResult result = new PageResult<>(); + result.setTotal(Long.parseLong(resp.getHeader("X-Total-Count").getValue())); + result.setList(respObj.getData()); + return result; + } catch (Exception e) { + log.error("list repository error", e); + throw new TimiException(TimiCode.ERROR, "list repository error", e); + } + } + + @Override + public Repository get(String repoName) { + try { + String respText = Request.get(API.REPO_GET.buildURL(new HashMap<>() {{ + put("owner", owner.getName()); + put("repoName", repoName); + }})).execute().returnContent().asString(); + return gson.fromJson(respText, Repository.class); + } catch (Exception e) { + log.error("get repository error", e); + throw new TimiException(TimiCode.ERROR, "get repository error", e); + } + } + + @Override + public List listBranches(String repoName) { + try { + String respText = Request.get(API.REPO_BRANCHES_LIST.buildURL(new HashMap<>() {{ + put("owner", owner.getName()); + put("repoName", repoName); + }})).execute().returnContent().asString(); + return gson.fromJson(respText, new TypeToken>() {}.getType()); + } catch (Exception e) { + log.error("list repository branches error", e); + throw new TimiException(TimiCode.ERROR, "list repository branches error", e); + } + } + + @Override + public List listFile(String repoName, String branch, String path) { + try { + String respText = Request.get(API.REPO_FILE_LIST.buildURL(new HashMap<>() {{ + put("owner", owner.getName()); + put("repoName", repoName); + put("path", path); + }}, new HashMap<>() {{ + put("ref", branch); + }})).execute().returnContent().asString(); + List list = gson.fromJson(respText, new TypeToken>() {}.getType()); + // 排序 + list.sort((f1, f2) -> { + if (f1.getType() == File.Type.dir && f2.getType() == File.Type.file) { + return -1; + } else { + if (f1.getType() == File.Type.file && f2.getType() == File.Type.dir) { + return 1; + } + return f1.getName().compareToIgnoreCase(f2.getName()); + } + }); + return list; + } catch (Exception e) { + log.error("list repository file error", e); + throw new TimiException(TimiCode.ERROR, "list repository file error", e); + } + } + + @Override + public InputStream getFileRaw(String repoName, String branch, String path) { + try { + ClassicHttpResponse resp = (ClassicHttpResponse) Request.get(API.REPO_FILE_RAW.buildURL(new HashMap<>() {{ + put("owner", owner.getName()); + put("repoName", repoName); + put("path", path); + }}, new HashMap<>() {{ + put("ref", branch); + }})).execute().returnResponse(); + return resp.getEntity().getContent(); + } catch (Exception e) { + log.error("get repository file raw error", e); + throw new TimiException(TimiCode.ERROR, "get repository file raw error", e); + } + } + + @Override + public InputStream getArchive(String repoName, String branch) { + try { + ClassicHttpResponse resp = (ClassicHttpResponse) Request.get(API.REPO_ARCHIVE.buildURL(new HashMap<>() {{ + put("owner", owner.getName()); + put("repoName", repoName); + put("archiveFormat", "%s.tar.gz".formatted(branch)); + }})).execute().returnResponse(); + return resp.getEntity().getContent(); + } catch (Exception e) { + log.error("get repository archive error", e); + throw new TimiException(TimiCode.ERROR, "get repository archive error", e); + } + } +} diff --git a/src/main/java/com/imyeyu/server/modules/git/util/GiteaTimestampAdapter.java b/src/main/java/com/imyeyu/server/modules/git/util/GiteaTimestampAdapter.java new file mode 100644 index 0000000..d0e41da --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/util/GiteaTimestampAdapter.java @@ -0,0 +1,28 @@ +package com.imyeyu.server.modules.git.util; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import java.lang.reflect.Type; +import java.time.Instant; + +/** + * @author 夜雨 + * @since 2025-06-26 11:40 + */ +public class GiteaTimestampAdapter implements JsonSerializer, JsonDeserializer { + + @Override + public Long deserialize(JsonElement json, Type type, JsonDeserializationContext context) { + return Instant.parse(json.getAsString()).toEpochMilli(); + } + + @Override + public JsonElement serialize(Long src, Type type, JsonSerializationContext context) { + return new JsonPrimitive(src); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/git/vo/developer/DeveloperRequest.java b/src/main/java/com/imyeyu/server/modules/git/vo/developer/DeveloperRequest.java new file mode 100644 index 0000000..003db80 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/vo/developer/DeveloperRequest.java @@ -0,0 +1,15 @@ +package com.imyeyu.server.modules.git.vo.developer; + +import lombok.Data; + +/** + * @author 夜雨 + * @since 2023-09-19 11:57 + */ +@Data +public class DeveloperRequest { + + private String email; + + private String rsa; +} diff --git a/src/main/java/com/imyeyu/server/modules/git/vo/issue/CommentPage.java b/src/main/java/com/imyeyu/server/modules/git/vo/issue/CommentPage.java new file mode 100644 index 0000000..048aa37 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/vo/issue/CommentPage.java @@ -0,0 +1,21 @@ +package com.imyeyu.server.modules.git.vo.issue; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.server.modules.common.entity.Comment; +import com.imyeyu.spring.bean.Page; + +/** + * @author 夜雨 + * @since 2024-02-28 14:27 + */ +@Data +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class CommentPage extends Page { + + private Comment.BizType bizType; + + private long bizId; +} diff --git a/src/main/java/com/imyeyu/server/modules/git/vo/issue/IssuePage.java b/src/main/java/com/imyeyu/server/modules/git/vo/issue/IssuePage.java new file mode 100644 index 0000000..153e7d1 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/vo/issue/IssuePage.java @@ -0,0 +1,21 @@ +package com.imyeyu.server.modules.git.vo.issue; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.server.modules.git.entity.Issue; +import com.imyeyu.spring.bean.Page; + +/** + * @author 夜雨 + * @since 2023-08-15 15:04 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class IssuePage extends Page { + + private long repositoryId; + + private Issue.Type type; + + private Issue.Status status; +} diff --git a/src/main/java/com/imyeyu/server/modules/git/vo/issue/IssueRequest.java b/src/main/java/com/imyeyu/server/modules/git/vo/issue/IssueRequest.java new file mode 100644 index 0000000..d34866b --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/vo/issue/IssueRequest.java @@ -0,0 +1,37 @@ +package com.imyeyu.server.modules.git.vo.issue; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import com.imyeyu.server.modules.git.entity.Issue; + +/** + * 工单反馈 + * + * @author 夜雨 + * @since 2023-08-15 15:51 + */ +@Data +public class IssueRequest { + + /** 工单 ID,在编辑时携带 */ + private Long id; + + /** 工单类型 */ + @NotNull + private Issue.Type type; + + /** 所属仓库 ID,创建工单时携带,编辑时不需要 */ + private Long repositoryId; + + /** 反馈用户昵称 */ + private String publisherNick; + + /** 版本 */ + private String version; + + /** 标题 */ + private String title; + + /** 反馈数据 */ + private String description; +} diff --git a/src/main/java/com/imyeyu/server/modules/git/vo/issue/IssueView.java b/src/main/java/com/imyeyu/server/modules/git/vo/issue/IssueView.java new file mode 100644 index 0000000..ccc469d --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/vo/issue/IssueView.java @@ -0,0 +1,22 @@ +package com.imyeyu.server.modules.git.vo.issue; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.server.modules.common.entity.Attachment; +import com.imyeyu.server.modules.common.vo.user.UserView; +import com.imyeyu.server.modules.git.entity.Issue; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2023-08-17 15:40 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class IssueView extends Issue { + + private UserView publisher; + + private List attachmentList; +} diff --git a/src/main/java/com/imyeyu/server/modules/git/vo/merge/MergePage.java b/src/main/java/com/imyeyu/server/modules/git/vo/merge/MergePage.java new file mode 100644 index 0000000..dbcb65c --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/vo/merge/MergePage.java @@ -0,0 +1,21 @@ +package com.imyeyu.server.modules.git.vo.merge; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.server.modules.git.entity.Merge; +import com.imyeyu.spring.bean.Page; + +/** + * @author 夜雨 + * @since 2023-08-17 11:52 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class MergePage extends Page { + + private long repositoryId; + + private Merge.Type type; + + private Merge.Status status; +} diff --git a/src/main/java/com/imyeyu/server/modules/git/vo/merge/MergeRequest.java b/src/main/java/com/imyeyu/server/modules/git/vo/merge/MergeRequest.java new file mode 100644 index 0000000..4022ed4 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/vo/merge/MergeRequest.java @@ -0,0 +1,44 @@ +package com.imyeyu.server.modules.git.vo.merge; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import com.imyeyu.server.modules.git.entity.Merge; + +/** + * 申请合并请求 + * + * @author 夜雨 + * @since 2023-08-17 11:43 + */ +@Data +public class MergeRequest { + + /** 和并请求 ID,编辑时携带 */ + private Long id; + + /** 所属仓库 ID */ + private long repositoryId; + + private Long issueId; + + /** 类型 */ + @NotNull(message = "请选择类型") + private Merge.Type type; + + /** 来源分支 */ + @NotBlank(message = "请选择来源分支") + private String fromBranch; + + /** 去向分支 */ + @NotBlank(message = "请选择去向分支") + private String toBranch; + + /** 说明标题 */ + @NotBlank(message = "请填写标题") + private String title; + + /** 合并说明 */ + @NotBlank(message = "请填写合并相关说明") + private String description; +} diff --git a/src/main/java/com/imyeyu/server/modules/git/vo/merge/MergeView.java b/src/main/java/com/imyeyu/server/modules/git/vo/merge/MergeView.java new file mode 100644 index 0000000..c5b2fc2 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/vo/merge/MergeView.java @@ -0,0 +1,25 @@ +package com.imyeyu.server.modules.git.vo.merge; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.server.modules.common.entity.Attachment; +import com.imyeyu.server.modules.common.vo.user.UserView; +import com.imyeyu.server.modules.git.entity.Issue; +import com.imyeyu.server.modules.git.entity.Merge; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2023-08-17 15:40 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class MergeView extends Merge { + + private Issue issue; + + private UserView requester; + + private List attachmentList; +} diff --git a/src/main/java/com/imyeyu/server/modules/git/vo/release/ReleasePage.java b/src/main/java/com/imyeyu/server/modules/git/vo/release/ReleasePage.java new file mode 100644 index 0000000..e7194f8 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/vo/release/ReleasePage.java @@ -0,0 +1,16 @@ +package com.imyeyu.server.modules.git.vo.release; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.bean.Page; + +/** + * @author 夜雨 + * @since 2023-08-16 11:46 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ReleasePage extends Page { + + private long repositoryId; +} diff --git a/src/main/java/com/imyeyu/server/modules/git/vo/release/ReleaseView.java b/src/main/java/com/imyeyu/server/modules/git/vo/release/ReleaseView.java new file mode 100644 index 0000000..3e5d846 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/vo/release/ReleaseView.java @@ -0,0 +1,19 @@ +package com.imyeyu.server.modules.git.vo.release; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.server.modules.common.entity.Attachment; +import com.imyeyu.server.modules.git.entity.Release; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2024-03-11 11:29 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ReleaseView extends Release { + + private List attachmentList; +} diff --git a/src/main/java/com/imyeyu/server/modules/git/vo/repository/RepositoryView.java b/src/main/java/com/imyeyu/server/modules/git/vo/repository/RepositoryView.java new file mode 100644 index 0000000..8278367 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/git/vo/repository/RepositoryView.java @@ -0,0 +1,19 @@ +package com.imyeyu.server.modules.git.vo.repository; + +import com.imyeyu.server.modules.git.bean.gitea.Branch; +import com.imyeyu.server.modules.git.bean.gitea.Repository; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2024-01-09 15:21 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class RepositoryView extends Repository { + + private List branchList; +} diff --git a/src/main/java/com/imyeyu/server/modules/gitea/bean/ActionLogDTO.java b/src/main/java/com/imyeyu/server/modules/gitea/bean/ActionLogDTO.java new file mode 100644 index 0000000..cdd3c58 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/gitea/bean/ActionLogDTO.java @@ -0,0 +1,63 @@ +package com.imyeyu.server.modules.gitea.bean; + +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.annotations.SerializedName; +import com.imyeyu.server.modules.gitea.util.GiteaUTCTimestampAdapter; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; + +/** + * @author 夜雨 + * @since 2025-06-27 15:37 + */ +@Data +public class ActionLogDTO { + + /** + * + * + * @author 夜雨 + * @since 2025-06-29 21:01 + */ + @Getter + @AllArgsConstructor + public enum Operation { + + PUSH_BRANCH(5); + + final int value; + } + + private int repoId; + + private String repoName; + + private int operatorId; + + private String refName; + + private String content; + + private Long operatedAt; + + /** + * + * + * @author 夜雨 + * @since 2025-06-27 16:20 + */ + @Data + public static class Commit { + + @SerializedName("Sha1") + private String sha; + + @SerializedName("Message") + private String message; + + @JsonAdapter(GiteaUTCTimestampAdapter.class) + @SerializedName("Timestamp") + private Long timestamp; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/gitea/entity/Action.java b/src/main/java/com/imyeyu/server/modules/gitea/entity/Action.java new file mode 100644 index 0000000..2d4a43b --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/gitea/entity/Action.java @@ -0,0 +1,23 @@ +package com.imyeyu.server.modules.gitea.entity; + +import lombok.Data; + +/** + * @author 夜雨 + * @since 2025-06-27 15:24 + */ +@Data +public class Action { + + private Long id; + private Long userId; + private Integer opType; + private Long actUserId; + private Long repoId; + private Long commentId; + private Boolean isDeleted; + private String refName; + private Boolean isPrivate; + private String content; + private Long createdUnix; +} diff --git a/src/main/java/com/imyeyu/server/modules/gitea/entity/Repository.java b/src/main/java/com/imyeyu/server/modules/gitea/entity/Repository.java new file mode 100644 index 0000000..823a849 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/gitea/entity/Repository.java @@ -0,0 +1,59 @@ +package com.imyeyu.server.modules.gitea.entity; + +import com.imyeyu.spring.annotation.table.Id; +import lombok.Data; + +/** + * @author 夜雨 + * @since 2025-06-29 21:19 + */ +@Data +public class Repository { + + @Id + private Long id; + private Long ownerId; + private String ownerName; + private String lowerName; + private String name; + private String description; + private String website; + private Integer originalServiceType; + private String originalUrl; + private String defaultBranch; + private String defaultWikiBranch; + private Integer numWatches; + private Integer numStars; + private Integer numForks; + private Integer numIssues; + private Integer numClosedIssues; + private Integer numPulls; + private Integer numClosedPulls; + private Integer numMilestones; + private Integer numClosedMilestones; + private Integer numProjects; + private Integer numClosedProjects; + private Integer numActionRuns; + private Integer numClosedActionRuns; + private Boolean isPrivate; + private Boolean isEmpty; + private Boolean isArchived; + private Boolean isMirror; + private Integer status; + private Boolean isFork; + private Long forkId; + private Boolean isTemplate; + private Long templateId; + private Long size; + private Long gitSize; + private Long lfsSize; + private Boolean isFsckEnabled; + private Boolean closeIssuesViaCommitInAnyBranch; + private String topics; + private String objectFormatName; + private Integer trustModel; + private String avatar; + private Long createdUnix; + private Long updatedUnix; + private Long archivedUnix; +} diff --git a/src/main/java/com/imyeyu/server/modules/gitea/entity/User.java b/src/main/java/com/imyeyu/server/modules/gitea/entity/User.java new file mode 100644 index 0000000..e6ba864 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/gitea/entity/User.java @@ -0,0 +1,152 @@ +package com.imyeyu.server.modules.gitea.entity; + +import com.imyeyu.spring.annotation.table.Id; +import lombok.Data; + +/** + * Gitea用户实体 + * @author 夜雨 + * @since 2025-07-03 14:49 + */ +@Data +public class User { + + /** ID */ + @Id + private Long id; + + /** 小写用户名 */ + private String lowerName; + + /** 用户名 */ + private String name; + + /** 全名 */ + private String fullName; + + /** 邮箱 */ + private String email; + + /** 是否保持邮箱私密 */ + private Boolean keepEmailPrivate; + + /** 邮件通知偏好设置 */ + private String emailNotificationsPreference; + + /** 密码 */ + private String passwd; + + /** 密码哈希算法 */ + private String passwdHashAlgo; + + /** 是否必须修改密码 */ + private Boolean mustChangePassword; + + /** 登录类型 */ + private Integer loginType; + + /** 登录源 */ + private Long loginSource; + + /** 登录名 */ + private String loginName; + + /** 类型 */ + private Integer type; + + /** 位置 */ + private String location; + + /** 网站 */ + private String website; + + /** 随机字符串 */ + private String rands; + + /** 盐值 */ + private String salt; + + /** 语言 */ + private String language; + + /** 描述 */ + private String description; + + /** 创建时间(Unix时间戳) */ + private Long createdUnix; + + /** 更新时间(Unix时间戳) */ + private Long updatedUnix; + + /** 最后登录时间(Unix时间戳) */ + private Long lastLoginUnix; + + /** 最后仓库可见性 */ + private Boolean lastRepoVisibility; + + /** 最大仓库创建数 */ + private Integer maxRepoCreation; + + /** 是否激活 */ + private Boolean isActive; + + /** 是否管理员 */ + private Boolean isAdmin; + + /** 是否受限 */ + private Boolean isRestricted; + + /** 是否允许Git钩子 */ + private Boolean allowGitHook; + + /** 是否允许导入本地仓库 */ + private Boolean allowImportLocal; + + /** 是否允许创建组织 */ + private Boolean allowCreateOrganization; + + /** 是否禁止登录 */ + private Boolean prohibitLogin; + + /** 头像URL */ + private String avatar; + + /** 头像邮箱 */ + private String avatarEmail; + + /** 是否使用自定义头像 */ + private Boolean useCustomAvatar; + + /** 关注者数量 */ + private Integer numFollowers; + + /** 正在关注数量 */ + private Integer numFollowing; + + /** 星标数量 */ + private Integer numStars; + + /** 仓库数量 */ + private Integer numRepos; + + /** 团队数量 */ + private Integer numTeams; + + /** 成员数量 */ + private Integer numMembers; + + /** 可见性 */ + private Integer visibility; + + /** 仓库管理员是否可以更改团队访问权限 */ + private Boolean repoAdminChangeTeamAccess; + + /** 差异视图样式 */ + private String diffViewStyle; + + /** 主题 */ + private String theme; + + /** 是否保持活动私密 */ + private Boolean keepActivityPrivate; +} diff --git a/src/main/java/com/imyeyu/server/modules/gitea/mapper/ActionMapper.java b/src/main/java/com/imyeyu/server/modules/gitea/mapper/ActionMapper.java new file mode 100644 index 0000000..c07a601 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/gitea/mapper/ActionMapper.java @@ -0,0 +1,16 @@ +package com.imyeyu.server.modules.gitea.mapper; + +import com.imyeyu.server.modules.gitea.bean.ActionLogDTO; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2025-06-27 15:26 + */ +public interface ActionMapper { + + long count(Long repoId, String branch, ActionLogDTO.Operation operation); + + List list(Long repoId, String branch, ActionLogDTO.Operation operation, long offset, int limit); +} diff --git a/src/main/java/com/imyeyu/server/modules/gitea/mapper/GiteaUserMapper.java b/src/main/java/com/imyeyu/server/modules/gitea/mapper/GiteaUserMapper.java new file mode 100644 index 0000000..f3d28cc --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/gitea/mapper/GiteaUserMapper.java @@ -0,0 +1,11 @@ +package com.imyeyu.server.modules.gitea.mapper; + +import com.imyeyu.server.modules.gitea.entity.User; +import com.imyeyu.spring.mapper.BaseMapper; + +/** + * @author 夜雨 + * @since 2025-07-03 14:56 + */ +public interface GiteaUserMapper extends BaseMapper { +} diff --git a/src/main/java/com/imyeyu/server/modules/gitea/mapper/RepositoryMapper.java b/src/main/java/com/imyeyu/server/modules/gitea/mapper/RepositoryMapper.java new file mode 100644 index 0000000..d63e2e8 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/gitea/mapper/RepositoryMapper.java @@ -0,0 +1,12 @@ +package com.imyeyu.server.modules.gitea.mapper; + +import com.imyeyu.server.modules.gitea.entity.Repository; +import com.imyeyu.spring.mapper.BaseMapper; + +/** + * @author 夜雨 + * @since 2025-06-30 00:17 + */ +public interface RepositoryMapper extends BaseMapper { + +} diff --git a/src/main/java/com/imyeyu/server/modules/gitea/service/GiteaService.java b/src/main/java/com/imyeyu/server/modules/gitea/service/GiteaService.java new file mode 100644 index 0000000..fb19d52 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/gitea/service/GiteaService.java @@ -0,0 +1,25 @@ +package com.imyeyu.server.modules.gitea.service; + +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.gitea.entity.Repository; +import com.imyeyu.server.modules.gitea.entity.User; +import com.imyeyu.server.modules.gitea.vo.ActionLogView; +import com.imyeyu.spring.bean.Page; +import com.imyeyu.spring.bean.PageResult; + +/** + * @author 夜雨 + * @since 2025-06-27 15:22 + */ +public interface GiteaService { + + User getOwner() throws TimiException; + + Repository getRepository(Long id) throws TimiException; + + Repository getRepositoryByName(String repoName) throws TimiException; + + PageResult pagePush(Page page) throws TimiException; + + PageResult pagePush(String repoName, String branch, Page page) throws TimiException; +} diff --git a/src/main/java/com/imyeyu/server/modules/gitea/service/implement/GiteaServiceImplement.java b/src/main/java/com/imyeyu/server/modules/gitea/service/implement/GiteaServiceImplement.java new file mode 100644 index 0000000..5ea067b --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/gitea/service/implement/GiteaServiceImplement.java @@ -0,0 +1,65 @@ +package com.imyeyu.server.modules.gitea.service.implement; + +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.gitea.bean.ActionLogDTO; +import com.imyeyu.server.modules.gitea.entity.Repository; +import com.imyeyu.server.modules.gitea.entity.User; +import com.imyeyu.server.modules.gitea.mapper.ActionMapper; +import com.imyeyu.server.modules.gitea.mapper.RepositoryMapper; +import com.imyeyu.server.modules.gitea.mapper.GiteaUserMapper; +import com.imyeyu.server.modules.gitea.service.GiteaService; +import com.imyeyu.server.modules.gitea.vo.ActionLogView; +import com.imyeyu.spring.bean.Page; +import com.imyeyu.spring.bean.PageResult; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * @author 夜雨 + * @since 2025-06-27 15:26 + */ +@Service +@RequiredArgsConstructor +public class GiteaServiceImplement implements GiteaService { + + private final ActionMapper actionMapper; + private final GiteaUserMapper giteaUserMapper; + private final RepositoryMapper repositoryMapper; + + @Override + public User getOwner() throws TimiException { + return giteaUserMapper.select(1L); + } + + @Override + public Repository getRepository(Long id) throws TimiException { + return repositoryMapper.select(id); + } + + @Override + public Repository getRepositoryByName(String repoName) throws TimiException { + Repository example = new Repository(); + example.setName(repoName); + return repositoryMapper.selectByExample(example); + } + + @Override + public PageResult pagePush(Page page) throws TimiException { + PageResult result = new PageResult<>(); + result.setTotal(actionMapper.count(null, null, ActionLogDTO.Operation.PUSH_BRANCH)); + result.setList(actionMapper.list(null, null, ActionLogDTO.Operation.PUSH_BRANCH, page.getOffset(), page.getLimit()).stream().map(ActionLogView::fromDTO).toList()); + return result; + } + + @Override + public PageResult pagePush(String repoName, String branch, Page page) throws TimiException { + Repository example = new Repository(); + example.setName(repoName); + Repository repo = repositoryMapper.selectByExample(example); + + PageResult result = new PageResult<>(); + result.setTotal(actionMapper.count(repo.getId(), branch, ActionLogDTO.Operation.PUSH_BRANCH)); + result.setList(actionMapper.list(repo.getId(), branch, ActionLogDTO.Operation.PUSH_BRANCH, page.getOffset(), page.getLimit()).stream().map(ActionLogView::fromDTO).toList()); + return result; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/gitea/util/GiteaUTCTimestampAdapter.java b/src/main/java/com/imyeyu/server/modules/gitea/util/GiteaUTCTimestampAdapter.java new file mode 100644 index 0000000..f94328f --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/gitea/util/GiteaUTCTimestampAdapter.java @@ -0,0 +1,32 @@ +package com.imyeyu.server.modules.gitea.util; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import java.lang.reflect.Type; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +/** + * + * + * @author 夜雨 + * @since 2025-06-27 16:18 + */ +public class GiteaUTCTimestampAdapter implements JsonDeserializer, JsonSerializer { + + @Override + public Long deserialize(JsonElement json, Type type, JsonDeserializationContext context) { + return OffsetDateTime.parse(json.getAsString()).toInstant().toEpochMilli(); + } + + @Override + public JsonElement serialize(Long timestamp, Type type, JsonSerializationContext context) { + return new JsonPrimitive(OffsetDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).toString()); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/gitea/vo/ActionLogView.java b/src/main/java/com/imyeyu/server/modules/gitea/vo/ActionLogView.java new file mode 100644 index 0000000..1829563 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/gitea/vo/ActionLogView.java @@ -0,0 +1,78 @@ +package com.imyeyu.server.modules.gitea.vo; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.reflect.TypeToken; +import com.imyeyu.java.TimiJava; +import com.imyeyu.server.TimiServerAPI; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.server.modules.common.vo.user.UserView; +import com.imyeyu.server.modules.gitea.bean.ActionLogDTO; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author 夜雨 + * @since 2025-06-27 15:31 + */ +@Data +public class ActionLogView { + + private int repoId; + + private String repoName; + + private String refName; + + private Long operatedAt; + + private List commitList; + + private UserView operator; + + public static ActionLogView fromDTO(ActionLogDTO dto) { + Gson gson = TimiServerAPI.applicationContext.getBean(Gson.class); + UserService userService = TimiServerAPI.applicationContext.getBean(UserService.class); + + ActionLogView view = new ActionLogView(); + view.setRepoId(dto.getRepoId()); + view.setRepoName(dto.getRepoName()); + view.setRefName(dto.getRefName()); + view.setOperatedAt(dto.getOperatedAt()); + view.setOperator(userService.view((long) dto.getOperatorId()).doFilter()); + { + if (TimiJava.isNotEmpty(dto.getContent())) { + JsonObject content = JsonParser.parseString(dto.getContent()).getAsJsonObject(); + List commitList = gson.fromJson(content.get("Commits"), new TypeToken>() {}.getType()); + view.setCommitList(new ArrayList<>()); + for (ActionLogDTO.Commit dtoCommit : commitList) { + Commit commit = new Commit(); + commit.setSha(dtoCommit.getSha()); + commit.setMessage(dtoCommit.getMessage().trim()); + commit.setCommittedAt(dtoCommit.getTimestamp()); + view.getCommitList().add(commit); + } + } + } + return view; + } + + /** + * + * + * @author 夜雨 + * @since 2025-06-27 15:37 + */ + @Data + public static class Commit { + + private String sha; + + private String message; + + private Long committedAt; + } +} \ No newline at end of file diff --git a/src/main/java/com/imyeyu/server/modules/lyric/controller/LyricController.java b/src/main/java/com/imyeyu/server/modules/lyric/controller/LyricController.java new file mode 100644 index 0000000..eaef28a --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/lyric/controller/LyricController.java @@ -0,0 +1,81 @@ +package com.imyeyu.server.modules.lyric.controller; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.io.IOSize; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.annotation.CaptchaValid; +import com.imyeyu.server.bean.CaptchaFrom; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.server.modules.lyric.entity.Lyric; +import com.imyeyu.server.modules.lyric.entity.LyricCorrect; +import com.imyeyu.server.modules.lyric.service.LyricCorrectService; +import com.imyeyu.server.modules.lyric.service.LyricService; +import com.imyeyu.server.modules.lyric.vo.LyricCorrectRequest; +import com.imyeyu.server.modules.lyric.vo.LyricRequest; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.annotation.RequiredToken; +import com.imyeyu.spring.bean.CaptchaData; +import com.imyeyu.spring.bean.Page; +import com.imyeyu.spring.bean.PageResult; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 歌词接口 + * + * @author 夜雨 + * @since 2023-02-02 11:50 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/lyric") +public class LyricController { + + private final UserService userService; + private final LyricService service; + private final LyricCorrectService correctService; + + @AOPLog + @RequestRateLimit + @PostMapping("") + public Lyric get(@Valid @RequestBody LyricRequest request) { + return service.get(request); + } + + @AOPLog + @CaptchaValid(CaptchaFrom.LYRIC_CORRECT) + @RequestRateLimit + @PostMapping("/correct") + public long correctRequest(CaptchaData request) { + if (IOSize.MB * 120 < request.getData().getFile().getSize()) { + throw new TimiException(TimiCode.ARG_BAD).msgKey("lyric.correct_request.too_big"); + } + return correctService.correctRequest(request.getData()); + } + + @AOPLog + @RequiredToken + @RequestRateLimit + @PostMapping("/correct/list") + public PageResult listCorrect(@Valid @RequestBody Page page) { + return correctService.pageByToken(page); + } + + @AOPLog + @RequiredToken + @RequestRateLimit + @PostMapping("/correct/cancel") + public void cancelCorrectRequest(@Min(1) @NotNull @RequestParam Long id) { + correctService.cancelCorrectRequest(id); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/lyric/entity/Lyric.java b/src/main/java/com/imyeyu/server/modules/lyric/entity/Lyric.java new file mode 100644 index 0000000..ad90949 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/lyric/entity/Lyric.java @@ -0,0 +1,28 @@ +package com.imyeyu.server.modules.lyric.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.entity.Entity; + +/** + * 歌词 + * + * @author 夜雨 + * @since 2023-02-02 11:54 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class Lyric extends Entity { + + /** 歌曲名称 */ + private String song; + + /** 演唱 */ + private String singer; + + /** 歌词 */ + private String data; + + /** 更新者 ID */ + private Long updatedBy; +} diff --git a/src/main/java/com/imyeyu/server/modules/lyric/entity/LyricCorrect.java b/src/main/java/com/imyeyu/server/modules/lyric/entity/LyricCorrect.java new file mode 100644 index 0000000..a78be32 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/lyric/entity/LyricCorrect.java @@ -0,0 +1,56 @@ +package com.imyeyu.server.modules.lyric.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.entity.Entity; + +/** + * 歌词更新请求 + * + * @author 夜雨 + * @since 2023-04-24 17:00 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class LyricCorrect extends Entity { + + /** 歌词 ID */ + private Long lyricId; + + /** 歌曲名 */ + private String song; + + /** 演唱 */ + private String singer; + + /** 歌词 */ + private String data; + + /** 申请人 */ + private Long requestBy; + + /** 申请 IP */ + private String requestIP; + + /** 取消时间 */ + private Long cancelAt; + + /** 通过时间 */ + private Long approvalAt; + + /** 拒绝时间 */ + private Long rejectAt; + + /** 拒绝原因 */ + private Long rejectReason; + + /** + * 是否可取消 + * + * @param requestBy 操作人 + * @return true 为可取消 + */ + public boolean canCancel(Long requestBy) { + return id != null && this.requestBy.equals(requestBy) && approvalAt == null && rejectAt == null; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/lyric/mapper/LyricCorrectMapper.java b/src/main/java/com/imyeyu/server/modules/lyric/mapper/LyricCorrectMapper.java new file mode 100644 index 0000000..6ef3780 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/lyric/mapper/LyricCorrectMapper.java @@ -0,0 +1,17 @@ +package com.imyeyu.server.modules.lyric.mapper; + +import com.imyeyu.server.modules.lyric.entity.LyricCorrect; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; + +/** + * 歌词更新请求 DAO + * + * @author 夜雨 + * @since 2023-04-24 17:06 + */ +public interface LyricCorrectMapper extends BaseMapper { + + @Select("UPDATE lyric_corrects SET cancel_at = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) WHERE id = #{id}") + void cancelCorrectRequest(long id); +} diff --git a/src/main/java/com/imyeyu/server/modules/lyric/mapper/LyricMapper.java b/src/main/java/com/imyeyu/server/modules/lyric/mapper/LyricMapper.java new file mode 100644 index 0000000..d0ae6ab --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/lyric/mapper/LyricMapper.java @@ -0,0 +1,22 @@ +package com.imyeyu.server.modules.lyric.mapper; + +import com.imyeyu.server.modules.lyric.entity.Lyric; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 歌词服务 + * + * @author 夜雨 + * @since 2023-02-02 11:54 + */ +public interface LyricMapper extends BaseMapper { + + @Select("SELECT * FROM lyric WHERE song LIKE CONCAT('%', #{song}, '%') OR singer LIKE CONCAT('%', #{singer}, '%') LIMIT 1") + Lyric selectBySong(String song, String singer); + + @Select("SELECT id, song, singer FROM lyric WHERE deleted_at IS NULL") + List listAll4Simple(); +} diff --git a/src/main/java/com/imyeyu/server/modules/lyric/service/LyricCorrectService.java b/src/main/java/com/imyeyu/server/modules/lyric/service/LyricCorrectService.java new file mode 100644 index 0000000..f358a43 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/lyric/service/LyricCorrectService.java @@ -0,0 +1,22 @@ +package com.imyeyu.server.modules.lyric.service; + +import com.imyeyu.server.modules.lyric.entity.LyricCorrect; +import com.imyeyu.server.modules.lyric.vo.LyricCorrectRequest; +import com.imyeyu.spring.bean.Page; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.CreatableService; + +/** + * 歌词更新请求服务 + * + * @author 夜雨 + * @since 2023-04-24 17:04 + */ +public interface LyricCorrectService extends CreatableService { + + PageResult pageByToken(Page page); + + long correctRequest(LyricCorrectRequest request); + + void cancelCorrectRequest(long id); +} diff --git a/src/main/java/com/imyeyu/server/modules/lyric/service/LyricService.java b/src/main/java/com/imyeyu/server/modules/lyric/service/LyricService.java new file mode 100644 index 0000000..c6bac71 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/lyric/service/LyricService.java @@ -0,0 +1,27 @@ +package com.imyeyu.server.modules.lyric.service; + +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.lyric.entity.Lyric; +import com.imyeyu.server.modules.lyric.vo.LyricRequest; +import com.imyeyu.spring.service.GettableService; + +import java.util.List; + +/** + * 歌词服务 + * + * @author 夜雨 + * @since 2023-02-02 11:55 + */ +public interface LyricService extends GettableService { + + /** + * 获取歌词 + * + * @return 歌词 + * @throws TimiException 服务异常 + */ + Lyric get(LyricRequest request); + + List listAll4Simple(); +} diff --git a/src/main/java/com/imyeyu/server/modules/lyric/service/implement/LyricCorrectServiceImplement.java b/src/main/java/com/imyeyu/server/modules/lyric/service/implement/LyricCorrectServiceImplement.java new file mode 100644 index 0000000..4d080bd --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/lyric/service/implement/LyricCorrectServiceImplement.java @@ -0,0 +1,108 @@ +package com.imyeyu.server.modules.lyric.service.implement; + +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.config.dbsource.TimiServerDBConfig; +import com.imyeyu.server.modules.common.entity.Attachment; +import com.imyeyu.server.modules.common.entity.User; +import com.imyeyu.server.modules.common.service.AttachmentService; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.server.modules.common.vo.attachment.AttachmentRequest; +import com.imyeyu.server.modules.lyric.entity.LyricCorrect; +import com.imyeyu.server.modules.lyric.mapper.LyricCorrectMapper; +import com.imyeyu.server.modules.lyric.service.LyricCorrectService; +import com.imyeyu.server.modules.lyric.vo.LyricCorrectRequest; +import com.imyeyu.spring.TimiSpring; +import com.imyeyu.spring.bean.Page; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +/** + * @author 夜雨 + * @since 2023-04-24 17:06 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class LyricCorrectServiceImplement extends AbstractEntityService implements LyricCorrectService { + + private final UserService userService; + private final AttachmentService attachmentService; + private final LyricCorrectMapper mapper; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public void create(LyricCorrect lyricCorrect) { + if (TimiJava.isNotEmpty(lyricCorrect.getRequestBy())) { + User user = userService.get(lyricCorrect.getRequestBy()); + if (user.isBanning()) { + throw new TimiException(TimiCode.RESULT_BAN).msgKey("user.banded"); + } + } + super.create(lyricCorrect); + } + + @Override + public PageResult pageByToken(Page page) { + throw new TimiException(TimiCode.ERROR_NOT_SUPPORT).msgKey("TODO"); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public long correctRequest(LyricCorrectRequest request) { + LyricCorrect correct = new LyricCorrect(); + correct.setLyricId(request.getLyricId()); + correct.setSong(request.getSong()); + correct.setSinger(request.getSinger()); + correct.setData(request.getData()); + correct.setRequestIP(TimiSpring.getRequestIP()); + + String token = TimiSpring.getToken(); + if (TimiJava.isNotEmpty(token)) { + User user = userService.getLoginUser(); + correct.setRequestBy(user.getId()); + } + create(correct); + try { + MultipartFile file = request.getFile(); + AttachmentRequest attachment = new AttachmentRequest(); + attachment.setBizType(Attachment.BizType.LYRIC); + attachment.setBizId(correct.getId()); + attachment.setName(correct.getId() + ".lrc"); + attachment.setAttachTypeValue(User.AttachType.WRAPPER); + attachment.setSize(file.getSize()); + attachment.setInputStream(file.getInputStream()); + attachmentService.create(attachment); + } catch (Exception e) { + log.error("save lyric error", e); + throw new TimiException(TimiCode.ERROR).msgKey("TODO save lyric error"); + } + return correct.getId(); + } + + @Override + public void cancelCorrectRequest(long id) { + User requestBy = userService.getLoginUser(); + LyricCorrect correct = mapper.select(id); + if (correct == null) { + throw new TimiException(TimiCode.RESULT_NULL).msgKey("lyric.correct_request.not_found"); + } + if (!correct.canCancel(requestBy.getId())) { + throw new TimiException(TimiCode.RESULT_BAD).msgKey("lyric.correct_request.unsupported_cancel"); + } + Attachment attachment = attachmentService.getByBizId(Attachment.BizType.LYRIC, correct.getId()); + attachmentService.delete(attachment.getId()); + mapper.cancelCorrectRequest(correct.getId()); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/lyric/service/implement/LyricServiceImplement.java b/src/main/java/com/imyeyu/server/modules/lyric/service/implement/LyricServiceImplement.java new file mode 100644 index 0000000..cc1af52 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/lyric/service/implement/LyricServiceImplement.java @@ -0,0 +1,140 @@ +package com.imyeyu.server.modules.lyric.service.implement; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.lyric.entity.Lyric; +import com.imyeyu.server.modules.lyric.mapper.LyricMapper; +import com.imyeyu.server.modules.lyric.service.LyricService; +import com.imyeyu.server.modules.lyric.vo.LyricRequest; +import com.imyeyu.utils.Collect; +import com.imyeyu.utils.Encoder; +import com.imyeyu.utils.Text; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.client5.http.fluent.Request; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 歌词服务实现 + * + * @author 夜雨 + * @since 2023-02-02 11:55 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class LyricServiceImplement implements LyricService { + + /** 酷狗歌曲查询接口 */ + private static final String API_GET_SONG = "http://mobilecdn.kugou.com/api/v3/search/song?"; + + /** 酷狗歌词接口 */ + private static final String API_GET_LRC = "http://m.kugou.com/app/i/krc.php"; + + /** 请求参数 */ + private static final Map HEADER = new HashMap<>(); + + static { + HEADER.put("host", "www.kugou.com"); + HEADER.put("Cookie", "kg_mid=38b475d7f7b7ff8c2cbc5d26d6b4eb92; kg_dfid=0PrIN23TsR430YtMHB12i9c8; kg_dfid_collect=d41d8cd98f00b204e9800998ecf8427e; Hm_lvt_aedee6983d4cfc62f509129360d6bb3d=1569245059,1569245104"); + HEADER.put("Accept-Charset", "UTF-8"); + } + + private final LyricMapper mapper; + private final Map allLyric; + + @Override + public Lyric get(Long id) { + return mapper.select(id); + } + + @Override + public Lyric get(LyricRequest request) { + // 从数据库模糊搜索获取 + Lyric lyric = mapper.selectBySong(request.getSong().trim(), request.getSinger().trim()); + if (lyric != null) { + return lyric; + } + { + // 从缓存模糊字符串搜索 + String name = request.getSong().trim() + "_" + request.getSinger().trim(); + Map result = new HashMap<>(); + for (Map.Entry item : allLyric.entrySet()) { + result.put(item.getKey(), Text.similarityRatio(name, item.getValue())); + } + LinkedHashMap sortedMap = Collect.sortMapByNumberValueDESC(result); + if (sortedMap.keySet().iterator().hasNext()) { + Long guessID = sortedMap.keySet().iterator().next(); + Number ratio = result.get(guessID); + if (.7 < ratio.doubleValue()) { + return this.get(guessID); + } + } + } + // 从 API 创建 + lyric = fetchLyric(request.getSong(), request.getSinger(), request.getTl()); + lyric.setCreatedAt(Time.now()); + mapper.insert(lyric); + return lyric; + } + + @Override + public List listAll4Simple() { + return mapper.listAll4Simple(); + } + + /** + * 从 API 获取 + * + * @param song 歌曲 + * @param siger 演唱 + * @param tl 时长(秒) + * @return 歌词 + * @throws TimiException 服务异常 + */ + private Lyric fetchLyric(String song, String siger, int tl) { + Map args = new HashMap<>(); + String response; + try { + // 获取 Hash + args.put("format", "json"); + args.put("keyword", song + " - " + siger); + args.put("page", "1"); + args.put("pagesize", "30"); + + Request request = Request.get(API_GET_SONG + Encoder.urlArgs(args)); + HEADER.forEach(request::addHeader); + response = request.execute().returnContent().asString(); + JsonObject data = JsonParser.parseString(response).getAsJsonObject().get("data").getAsJsonObject().get("info").getAsJsonArray().get(0).getAsJsonObject(); + + args.clear(); + args.put("cmd", "100"); + args.put("keyword", data.get("songname").getAsString()); + args.put("hash", data.get("hash").getAsString()); + args.put("timelength", tl); + args.put("d", String.valueOf(Math.random())); + request = Request.get(API_GET_LRC + Encoder.urlArgs(args)); + response = request.execute().returnContent().asString(); + if (response.startsWith("\uFEFF")) { + response = response.substring(1); + } + + Lyric lyric = new Lyric(); + lyric.setSong(data.get("songname").getAsString()); + lyric.setSinger(data.get("singername").getAsString()); + lyric.setData(response); + return lyric; + } catch (Exception e) { + log.error("fetch lyric fail", e); + throw new TimiException(TimiCode.ERROR).msgKey("无法获取歌词"); + } + } +} diff --git a/src/main/java/com/imyeyu/server/modules/lyric/util/InitLyricSearch.java b/src/main/java/com/imyeyu/server/modules/lyric/util/InitLyricSearch.java new file mode 100644 index 0000000..a53e38a --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/lyric/util/InitLyricSearch.java @@ -0,0 +1,43 @@ +package com.imyeyu.server.modules.lyric.util; + +import com.imyeyu.server.modules.lyric.entity.Lyric; +import com.imyeyu.server.modules.lyric.service.LyricService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 初始化歌词缓存,用于编辑距离算法的模糊搜索 + * + * @author 夜雨 + * @since 2023-02-03 16:59 + */ +@Component +public class InitLyricSearch implements CommandLineRunner { + + @Lazy + @Autowired + private LyricService service; + + private final Map allLyric = new HashMap<>(); + + @Override + public void run(String... args) throws Exception { + List list = service.listAll4Simple(); + for (int i = 0; i < list.size(); i++) { + allLyric.put(list.get(i).getId(), list.get(i).getSong() + "_" + list.get(i).getSinger()); + } + } + + /** @return 所有歌词映射,Map<ID, Song_Singer> */ + @Bean("allLyric") + public Map getAllLyric() { + return allLyric; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/lyric/vo/LyricCorrectRequest.java b/src/main/java/com/imyeyu/server/modules/lyric/vo/LyricCorrectRequest.java new file mode 100644 index 0000000..1427ab4 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/lyric/vo/LyricCorrectRequest.java @@ -0,0 +1,37 @@ +package com.imyeyu.server.modules.lyric.vo; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +/** + * @author 夜雨 + * @since 2023-05-07 22:38 + */ +@Data +public class LyricCorrectRequest { + + /** 歌词 ID */ + private Long lyricId; + + /** 歌曲名 */ + @NotBlank + private String song; + + /** 演唱 */ + @NotBlank + private String singer; + + /** 歌词 */ + @NotBlank + private String data; + + /** 编码 */ + @NotBlank + private String encode; + + /** 歌曲文件 */ + @NotNull + private MultipartFile file; +} diff --git a/src/main/java/com/imyeyu/server/modules/lyric/vo/LyricRequest.java b/src/main/java/com/imyeyu/server/modules/lyric/vo/LyricRequest.java new file mode 100644 index 0000000..50e5f69 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/lyric/vo/LyricRequest.java @@ -0,0 +1,29 @@ +package com.imyeyu.server.modules.lyric.vo; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 获取歌词视图对象 + * + * @author 夜雨 + * @since 2023-02-02 14:12 + */ +@Data +public class LyricRequest { + + /** 歌曲名 */ + @NotBlank(message = "lyric.song.empty") + private String song; + + /** 演唱 */ + @NotBlank(message = "lyric.singer.empty") + private String singer; + + /** 时长 */ + @Min(value = 0, message = "lyric.tl.min") + @NotNull(message = "lyric.tl.empty") + private Integer tl; +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/annotation/RequiredFMCServerToken.java b/src/main/java/com/imyeyu/server/modules/minecraft/annotation/RequiredFMCServerToken.java new file mode 100644 index 0000000..6134d7a --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/annotation/RequiredFMCServerToken.java @@ -0,0 +1,20 @@ +package com.imyeyu.server.modules.minecraft.annotation; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 需要令牌 + * + * @author 夜雨 + * @since 2024-08-06 16:38 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Component +public @interface RequiredFMCServerToken { +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/annotation/RequiredFMCServerTokenInterceptor.java b/src/main/java/com/imyeyu/server/modules/minecraft/annotation/RequiredFMCServerTokenInterceptor.java new file mode 100644 index 0000000..e2f3755 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/annotation/RequiredFMCServerTokenInterceptor.java @@ -0,0 +1,51 @@ +package com.imyeyu.server.modules.minecraft.annotation; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.spring.TimiSpring; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +/** + * Minecraft 服务器接口验证令牌 + * + * @author 夜雨 + * @since 2024-08-06 16:40 + */ +@Component +@RequiredArgsConstructor +public class RequiredFMCServerTokenInterceptor implements HandlerInterceptor { + + private final SettingService settingService; + + /** + * 处理请求 + * + * @param req 请求 + * @param resp 返回 + * @param handler 处理方法 + * @return true 为通过 + */ + @Override + public boolean preHandle(@NonNull HttpServletRequest req, @NonNull HttpServletResponse resp, @NonNull Object handler) { + // 方法注解 + if (handler instanceof HandlerMethod handlerMethod) { + RequiredFMCServerToken requiredFMCServerToken = handlerMethod.getMethodAnnotation(RequiredFMCServerToken.class); + if (requiredFMCServerToken == null) { + return true; + } + if (!settingService.getAsString(SettingKey.FMC_SERVER_TOKEN).equals(TimiSpring.getToken())) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("token.illegal"); + } + return true; + } + return true; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/bean/AttachType.java b/src/main/java/com/imyeyu/server/modules/minecraft/bean/AttachType.java new file mode 100644 index 0000000..7086f03 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/bean/AttachType.java @@ -0,0 +1,12 @@ +package com.imyeyu.server.modules.minecraft.bean; + +/** + * @author 夜雨 + * @since 2024-04-28 22:06 + */ +public enum AttachType { + + LAUNCHER_BG, + + LAUNCHER_BGM +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/controller/MinecraftController.java b/src/main/java/com/imyeyu/server/modules/minecraft/controller/MinecraftController.java new file mode 100644 index 0000000..d3f8e4d --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/controller/MinecraftController.java @@ -0,0 +1,78 @@ +package com.imyeyu.server.modules.minecraft.controller; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.annotation.EnableSetting; +import com.imyeyu.server.modules.blog.util.UserToken; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.minecraft.service.FMCImageMapService; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.annotation.RequiredToken; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * Minecraft 相关接口 + * + * @author 夜雨 + * @since 2021-05-20 00:24 + */ +@Slf4j +@Validated +@RestController +@RequestMapping("/fmc") +public class MinecraftController { + + @Autowired + private FMCImageMapService fmcImageMapService; + + @Autowired + private UserToken userToken; + + /** + * 上传 FMCImageMap 插件图床图片 + * + * @param file 文件 + * @return 头像资源路径 + */ + @AOPLog + @RequiredToken + @EnableSetting(value = SettingKey.FMC_ENABLE_IMAGE_MAP_UPLOAD, message = "minecraft.fmc_image_map.off_service") + @RequestRateLimit + @PostMapping("/imagemap/upload") + public String uploadFMCImageMap(@NotNull @RequestParam("request") MultipartFile file) { + if (file.getOriginalFilename() != null && !file.getOriginalFilename().endsWith(".png")) { + throw new TimiException(TimiCode.ARG_BAD).msgKey("minecraft.fmc_image_map.png_only"); + } + return fmcImageMapService.upload(file); + } + + @AOPLog + @RequiredToken + @RequestRateLimit + @PostMapping("/imagemap/list") + public List listAllFMCImageMap() { + return fmcImageMapService.findAll(); + } + + @AOPLog + @RequiredToken + @RequestRateLimit + @PostMapping("/imagemap/delete") + public void deleteFMCImageMap(@NotBlank @Valid @RequestBody String fileName) { + fmcImageMapService.delete(fileName); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/controller/PackController.java b/src/main/java/com/imyeyu/server/modules/minecraft/controller/PackController.java new file mode 100644 index 0000000..d8785e2 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/controller/PackController.java @@ -0,0 +1,47 @@ +package com.imyeyu.server.modules.minecraft.controller; + +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiResponse; +import com.imyeyu.server.modules.minecraft.entity.MinecraftPack; +import com.imyeyu.server.modules.minecraft.entity.MinecraftPackSource; +import com.imyeyu.server.modules.minecraft.service.PackService; +import com.imyeyu.spring.annotation.RequestRateLimit; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 客户端接口,通常是 ForeverMC 启动器请求 + * + * @author 夜雨 + * @since 2023-06-13 17:15 + */ +@Slf4j +@RestController +@RequestMapping("/fmc/pack") +public class PackController { + + @Autowired + private PackService service; + + /** + * 可用客户端列表 + * + * @return 客户端列表 + */ + @RequestRateLimit + @RequestMapping("/list") + public TimiResponse listPacks() { + List list = service.listPacks(); + for (int i = 0; i < list.size(); i++) { + List sourceList = list.get(i).getSourceList(); + for (int j = 0; j < sourceList.size(); j++) { + sourceList.get(j).setOrder(null); + } + } + return new TimiResponse<>(TimiCode.SUCCESS, list); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/controller/PlayerController.java b/src/main/java/com/imyeyu/server/modules/minecraft/controller/PlayerController.java new file mode 100644 index 0000000..69e0460 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/controller/PlayerController.java @@ -0,0 +1,127 @@ +package com.imyeyu.server.modules.minecraft.controller; + +import jakarta.annotation.PostConstruct; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.annotation.EnableSetting; +import com.imyeyu.server.config.RedisConfig; +import com.imyeyu.server.modules.blog.util.UserToken; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.server.modules.minecraft.entity.MinecraftPlayer; +import com.imyeyu.server.modules.minecraft.service.PlayerService; +import com.imyeyu.server.modules.minecraft.vo.TokenRequest; +import com.imyeyu.server.modules.minecraft.vo.TokenResponse; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.annotation.RequestSingleParam; +import com.imyeyu.spring.annotation.RequiredToken; +import com.imyeyu.spring.util.Redis; +import com.imyeyu.spring.util.RedisSerializers; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 玩家控制(登录或账号管理) + * + * @author 夜雨 + * @since 2024-03-29 12:55 + */ +@RestController +@RequestMapping("/fmc/player") +@RequiredArgsConstructor +public class PlayerController { + + private final RedisConfig redisConfig; + + private final UserService userService; + private final PlayerService service; + + private final UserToken userToken; + + private Redis redis; + + @PostConstruct + private void postConstruct() { + redis = redisConfig.getRedis(redisConfig.getDatabase().getFmcPlayerToken(), RedisSerializers.STRING, RedisSerializers.LONG); + } + + /** + * 该玩家名是否已绑定 + * + * @param name 玩家名 + * @return true 为已绑定 + */ + @RequestRateLimit + @EnableSetting(value = SettingKey.FMC_PLAYER_LOGIN_ENABLE, message = "登录服务未启用") + @GetMapping("/bound/{name}") + public boolean isBound(@PathVariable("name") String name) { + return service.getByName(name) != null; + } + + @AOPLog + @RequiredToken + @RequestRateLimit + @PostMapping("/bind") + public void bind(@RequestSingleParam String name) { + MinecraftPlayer player = new MinecraftPlayer(); + player.setName(name); + player.setUserId(userService.getLoginUser().getId()); + service.create(player); + } + + @AOPLog + @RequiredToken + @RequestRateLimit + @PostMapping("/unbind") + public void unbind(@RequestSingleParam Long id) { + service.listByUserId(userService.getLoginUser().getId()) + .stream() + .filter(player -> player.getId().equals(id)) + .findFirst() + .ifPresent(player -> service.delete(player.getId())); + } + + @RequestRateLimit + @PostMapping("/list") + public List listPlayer(@RequestHeader("Token") String token) { + Long userId = TimiJava.firstNotNull(redis.get(token), userToken.getUserId(token)); + if (userId == null) { + throw new TimiException(TimiCode.RESULT_BAD).msgKey("token.illegal"); + } + List result = service.listByUserId(userId); + for (int i = 0; i < result.size(); i++) { + MinecraftPlayer player = result.get(i); + player.setLastLoginIP(null); + player.setCreatedAt(null); + player.setUpdatedAt(null); + } + return result; + } + + @AOPLog + @RequestRateLimit + @EnableSetting(value = SettingKey.FMC_PLAYER_LOGIN_ENABLE, message = "登录服务未启用") + @PostMapping("/login") + public TokenResponse login(@RequestSingleParam Long playerId, @RequestHeader("Token") String token) { + return service.login(playerId, token); + } + + @AOPLog + @RequestRateLimit + @EnableSetting(value = SettingKey.FMC_PLAYER_LOGIN_ENABLE, message = "登录服务未启用") + @PostMapping("/login/token") + public TokenResponse genLoginToken(@Valid @RequestBody TokenRequest request) { + return service.genLoginToken(request); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/entity/MinecraftPack.java b/src/main/java/com/imyeyu/server/modules/minecraft/entity/MinecraftPack.java new file mode 100644 index 0000000..b8895c9 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/entity/MinecraftPack.java @@ -0,0 +1,45 @@ +package com.imyeyu.server.modules.minecraft.entity; + +import com.imyeyu.spring.entity.Entity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * 整合包 + * + * @author 夜雨 + * @since 2023-06-13 17:17 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class MinecraftPack extends Entity { + + /** 名称 */ + private String name; + + /** 整合包版本 */ + private String ver; + + /** 标题 */ + private String title; + + /** 说明 */ + private String description; + + /** 游戏版本 */ + private String gameVer; + + /** 默认配置 */ + private String defOption; + + /** 文件大小 */ + private long size; + + /** true 为已过时 */ + private boolean isDeprecated; + + /** 下载源列表 */ + private List sourceList; +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/entity/MinecraftPackSource.java b/src/main/java/com/imyeyu/server/modules/minecraft/entity/MinecraftPackSource.java new file mode 100644 index 0000000..11c8848 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/entity/MinecraftPackSource.java @@ -0,0 +1,47 @@ +package com.imyeyu.server.modules.minecraft.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.entity.Entity; + +/** + * 整合包下载源 + * + * @author 夜雨 + * @since 2023-06-13 17:32 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class MinecraftPackSource extends Entity { + + /** + * + * + * @author 夜雨 + * @since 2024-06-18 12:53 + */ + public enum Type { + + ATTACH, + + URL + } + + /** 整合包 ID */ + private Long packId; + + /** 名称 */ + private String name; + + /** 类型 */ + private Type type; + + /** 地址 */ + private String data; + + /** 排序 */ + private Integer order; + + /** true 为默认 */ + private boolean isDefault; +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/entity/MinecraftPlayer.java b/src/main/java/com/imyeyu/server/modules/minecraft/entity/MinecraftPlayer.java new file mode 100644 index 0000000..84492a0 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/entity/MinecraftPlayer.java @@ -0,0 +1,28 @@ +package com.imyeyu.server.modules.minecraft.entity; + +import com.imyeyu.spring.entity.Entity; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * @author 夜雨 + * @since 2024-03-29 10:08 + */ +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class MinecraftPlayer extends Entity { + + private Long userId; + + private String name; + + private String lastLoginIP; + + private Long lastLoginAt; + + public MinecraftPlayer(Long userId) { + this.userId = userId; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/mapper/PackMapper.java b/src/main/java/com/imyeyu/server/modules/minecraft/mapper/PackMapper.java new file mode 100644 index 0000000..2ae9483 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/mapper/PackMapper.java @@ -0,0 +1,14 @@ +package com.imyeyu.server.modules.minecraft.mapper; + +import com.imyeyu.server.modules.minecraft.entity.MinecraftPack; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2023-06-13 17:34 + */ +public interface PackMapper { + + List listPacks(); +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/mapper/PlayerMapper.java b/src/main/java/com/imyeyu/server/modules/minecraft/mapper/PlayerMapper.java new file mode 100644 index 0000000..a593c31 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/mapper/PlayerMapper.java @@ -0,0 +1,20 @@ +package com.imyeyu.server.modules.minecraft.mapper; + +import com.imyeyu.server.modules.minecraft.entity.MinecraftPlayer; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2024-03-29 10:08 + */ +public interface PlayerMapper extends BaseMapper { + + @Select("SELECT * FROM minecraft_player WHERE user_id = #{userId}" + NOT_DELETE) + List listByUserId(Long userId); + + @Select("SELECT * FROM minecraft_player WHERE name = #{name}" + NOT_DELETE + LIMIT_1) + MinecraftPlayer selectByName(String name); +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/service/FMCImageMapService.java b/src/main/java/com/imyeyu/server/modules/minecraft/service/FMCImageMapService.java new file mode 100644 index 0000000..ef46010 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/service/FMCImageMapService.java @@ -0,0 +1,38 @@ +package com.imyeyu.server.modules.minecraft.service; + +import com.imyeyu.java.bean.timi.TimiException; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2023-07-15 18:11 + */ +public interface FMCImageMapService { + + /** + * 上传 FMCImageMap 插件图床图片 + * + * @param file 上传文件对象 + * @return 回调该资源路径 + * @throws TimiException 服务异常 + */ + String upload(MultipartFile file); + + /** + * 获取该用户上传 FMCImageMap 插件图床的图片 + * + * @return 文件名 + * @throws TimiException 服务异常 + */ + List findAll(); + + /** + * 删除上传 IMCImgMap 插件图床的图片 + * + * @param fileName 图片文件名 + * @throws TimiException 服务异常 + */ + void delete(String fileName); +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/service/PackService.java b/src/main/java/com/imyeyu/server/modules/minecraft/service/PackService.java new file mode 100644 index 0000000..95bc5f1 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/service/PackService.java @@ -0,0 +1,14 @@ +package com.imyeyu.server.modules.minecraft.service; + +import com.imyeyu.server.modules.minecraft.entity.MinecraftPack; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2023-06-13 17:33 + */ +public interface PackService { + + List listPacks(); +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/service/PlayerService.java b/src/main/java/com/imyeyu/server/modules/minecraft/service/PlayerService.java new file mode 100644 index 0000000..f68f701 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/service/PlayerService.java @@ -0,0 +1,38 @@ +package com.imyeyu.server.modules.minecraft.service; + +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.minecraft.entity.MinecraftPlayer; +import com.imyeyu.server.modules.minecraft.vo.TokenRequest; +import com.imyeyu.server.modules.minecraft.vo.TokenResponse; +import com.imyeyu.spring.service.CreatableService; +import com.imyeyu.spring.service.DeletableService; + +import java.util.List; + +/** + * @author 夜雨 + * @version 2024-03-29 10:08 + */ +public interface PlayerService extends CreatableService, DeletableService { + + /** + * 根据绑定玩家名获取玩家 + * + * @param name 绑定玩家名 + * @return 玩家 + * @throws TimiException 服务异常 + */ + MinecraftPlayer getByName(String name) throws TimiException; + + /** + * 获取玩家 + * + * @return 玩家 + * @throws TimiException 服务异常 + */ + List listByUserId(Long userId) throws TimiException; + + TokenResponse login(Long playerId, String loginToken) throws TimiException; + + TokenResponse genLoginToken(TokenRequest request) throws TimiException; +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/service/implement/FMCImageMapServiceImplement.java b/src/main/java/com/imyeyu/server/modules/minecraft/service/implement/FMCImageMapServiceImplement.java new file mode 100644 index 0000000..fe5d2d8 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/service/implement/FMCImageMapServiceImplement.java @@ -0,0 +1,62 @@ +package com.imyeyu.server.modules.minecraft.service.implement; + +import lombok.RequiredArgsConstructor; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.server.modules.minecraft.service.FMCImageMapService; +import com.imyeyu.server.modules.system.service.FileService; +import com.imyeyu.utils.OS; +import org.jcodec.api.NotSupportedException; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * @author 夜雨 + * @version 2023-07-15 18:11 + */ +@Service +@RequiredArgsConstructor +public class FMCImageMapServiceImplement implements FMCImageMapService, OS.FileSystem { + + private final UserService userService; + private final FileService fileService; + + @Override + public String upload(MultipartFile file) { + throw new NotSupportedException("TODO update"); +// try { +// User user = userService.getLoginUser(); +// BufferedImage img = ImageIO.read(file.getInputStream()); +// // 限制尺寸为 128 +// if (img.getWidth() != 128 || img.getHeight() != 128) { +// throw new TimiException(TimiCode.ARG_BAD).msgKey("图片尺寸需为 128 × 128 像素"); +// } +// // 文件夹 +// fileService.mkdirs(config.getResources().getMcMap() + SEP + user.getId() + SEP); +// // 处理文件 +// ResourceFile res = new ResourceFile(); +// res.setName(System.currentTimeMillis() + "_" + Digest.md5(String.valueOf(Math.random())).substring(0, 8) + ".png"); +// res.setPath(config.getResources().getMcMap() + SEP + user.getId() + SEP); +// res.setInputStream(file.getInputStream()); +// fileService.upload(res); +// return res.getFullPath(); +// } catch (Exception e) { +// throw new TimiException(TimiCode.ERROR).msgKey("上传文件异常:" + e.getMessage()); +// } + } + + @Override + public List findAll() { + throw new NotSupportedException("TODO update"); +// User user = userService.getLoginUser(); +// return fileService.list4TimiServer(config.getResources().getMcMap() + SEP + user.getId() + SEP).stream().map(File::getName).collect(Collectors.toList()); + } + + @Override + public void delete(String fileName) { + throw new NotSupportedException("TODO update"); +// User user = userService.getLoginUser(); +// fileService.destroy(config.getResources().getMcMap() + SEP + user.getId() + SEP + fileName); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/service/implement/PackServiceImplement.java b/src/main/java/com/imyeyu/server/modules/minecraft/service/implement/PackServiceImplement.java new file mode 100644 index 0000000..7a7e59d --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/service/implement/PackServiceImplement.java @@ -0,0 +1,25 @@ +package com.imyeyu.server.modules.minecraft.service.implement; + +import com.imyeyu.server.modules.minecraft.entity.MinecraftPack; +import com.imyeyu.server.modules.minecraft.mapper.PackMapper; +import com.imyeyu.server.modules.minecraft.service.PackService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author 夜雨 + * @version 2023-06-13 17:34 + */ +@Service +public class PackServiceImplement implements PackService { + + @Autowired + private PackMapper mapper; + + @Override + public List listPacks() { + return mapper.listPacks(); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/service/implement/PlayerServiceImplement.java b/src/main/java/com/imyeyu/server/modules/minecraft/service/implement/PlayerServiceImplement.java new file mode 100644 index 0000000..ab76b8d --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/service/implement/PlayerServiceImplement.java @@ -0,0 +1,151 @@ +package com.imyeyu.server.modules.minecraft.service.implement; + +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.config.RedisConfig; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.entity.User; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.server.modules.common.service.UserService; +import com.imyeyu.server.modules.minecraft.entity.MinecraftPlayer; +import com.imyeyu.server.modules.minecraft.mapper.PlayerMapper; +import com.imyeyu.server.modules.minecraft.service.PlayerService; +import com.imyeyu.server.modules.minecraft.vo.TokenRequest; +import com.imyeyu.server.modules.minecraft.vo.TokenResponse; +import com.imyeyu.spring.TimiSpring; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.spring.util.Redis; +import com.imyeyu.spring.util.RedisSerializers; +import com.imyeyu.utils.Calc; +import com.imyeyu.utils.Time; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +/** + * @author 夜雨 + * @version 2024-03-29 10:08 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PlayerServiceImplement extends AbstractEntityService implements PlayerService { + + private final RedisConfig redisConfig; + + private final UserService userService; + private final SettingService settingService; + + private final PlayerMapper mapper; + + private Redis redis; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @PostConstruct + private void postConstruct() { + redis = redisConfig.getRedis(redisConfig.getDatabase().getFmcPlayerToken(), RedisSerializers.STRING, RedisSerializers.LONG); + } + + @Override + public void create(MinecraftPlayer player) { + if (player.getName().contains(" ")) { + throw new TimiException(TimiCode.ARG_BAD).msgKey("绑定玩家昵称不允许有空格"); + } + // 自查重 + List playerList = listByUserId(player.getUserId()); + if (settingService.getAsInt(SettingKey.FMC_MAX_BIND) <= playerList.size()) { + throw new TimiException(TimiCode.RESULT_BAD).msgKey("已达到最大绑定数量:%s 个".formatted(settingService.getAsInt(SettingKey.FMC_MAX_BIND))); + } + for (int i = 0; i < playerList.size(); i++) { + if (playerList.get(i).getName().equals(player.getName())) { + throw new TimiException(TimiCode.RESULT_BAD).msgKey("已绑定此玩家名称"); + } + } + // 全局查重 + MinecraftPlayer playerByName = getByName(player.getName()); + if (playerByName != null && !playerByName.getUserId().equals(player.getUserId())) { + throw new TimiException(TimiCode.DATA_EXIST).msgKey("此玩家昵称已被绑定,请使用其他昵称"); + } + MinecraftPlayer dbPlayer = new MinecraftPlayer(); + dbPlayer.setUserId(player.getUserId()); + dbPlayer.setName(player.getName()); + super.create(dbPlayer); + } + + @Override + public MinecraftPlayer getByName(String name) throws TimiException { + return mapper.selectByName(name); + } + + @Override + public List listByUserId(Long userId) throws TimiException { + return mapper.listByUserId(userId); + } + + @Override + public TokenResponse login(Long playerId, String loginToken) throws TimiException { + Long userId = redis.get(loginToken); + if (userId == null) { + throw new TimiException(TimiCode.RESULT_BAD).msgKey("token.illegal"); + } + List playerList = listByUserId(userId); + MinecraftPlayer player = null; + for (int i = 0; i < playerList.size(); i++) { + if (playerList.get(i).getId().equals(playerId)) { + player = playerList.get(i); + break; + } + } + if (player == null) { + throw new TimiException(TimiCode.RESULT_BAD).msgKey("token.illegal"); + } + player.setLastLoginIP(TimiSpring.getRequestIP()); + player.setLastLoginAt(Time.now()); + mapper.update(player); + + String playerToken = UUID.randomUUID().toString(); + long ttl = settingService.getAsInt(SettingKey.FMC_PLAYER_LOGIN_TOKEN_TTL) * Time.D; + redis.set(playerToken, playerId, ttl); + return new TokenResponse(userId, playerToken, Time.now() + ttl); + } + + @Override + public TokenResponse genLoginToken(TokenRequest request) throws TimiException { + String reqUser = request.getUser(); + + User user; + if (Calc.isNumber(reqUser)) { + user = userService.get(Long.valueOf(reqUser)); + } else if (reqUser.contains("@")) { + user = userService.getByEmail(reqUser); + } else { + MinecraftPlayer playerByName = getByName(reqUser); + if (playerByName == null) { + throw new TimiException(TimiCode.RESULT_NULL).msgKey("not found player"); + } + user = userService.get(playerByName.getUserId()); + } + if (user == null) { + throw new TimiException(TimiCode.RESULT_NULL).msgKey("未注册或绑定此玩家昵称,请到 https://%s 进行注册或绑定".formatted(settingService.getAsString(SettingKey.DOMAIN_SPACE))); + } + if (user.isBanning()) { + throw new TimiException(TimiCode.RESULT_BAN).msgKey("账号封禁中"); + } + if (userService.isInvalidPassword(user.getPassword(), request.getPassword())) { + throw new TimiException(TimiCode.ARG_BAD).msgKey("密码错误"); + } + String token = UUID.randomUUID().toString(); + long ttl = settingService.getAsInt(SettingKey.FMC_PLAYER_LOGIN_TOKEN_TTL) * Time.D; + redis.set(token, user.getId(), ttl); + return new TokenResponse(user.getId(), token, Time.now() + ttl); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/vo/LoginRequest.java b/src/main/java/com/imyeyu/server/modules/minecraft/vo/LoginRequest.java new file mode 100644 index 0000000..2f796ac --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/vo/LoginRequest.java @@ -0,0 +1,15 @@ +package com.imyeyu.server.modules.minecraft.vo; + +import lombok.Data; + +/** + * @author 夜雨 + * @version 2024-03-29 14:14 + */ +@Data +public class LoginRequest { + + private Long playerId; + + private String token; +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/vo/TokenRequest.java b/src/main/java/com/imyeyu/server/modules/minecraft/vo/TokenRequest.java new file mode 100644 index 0000000..c87d4b6 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/vo/TokenRequest.java @@ -0,0 +1,18 @@ +package com.imyeyu.server.modules.minecraft.vo; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * @author 夜雨 + * @version 2024-03-29 14:14 + */ +@Data +public class TokenRequest { + + @NotBlank(message = "todo.msg") + private String user; + + @NotBlank(message = "todo.msg") + private String password; +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/vo/TokenResponse.java b/src/main/java/com/imyeyu/server/modules/minecraft/vo/TokenResponse.java new file mode 100644 index 0000000..3d889da --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/vo/TokenResponse.java @@ -0,0 +1,26 @@ +package com.imyeyu.server.modules.minecraft.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 令牌返回 + * + * @author 夜雨 + * @since 2024-03-29 10:13 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TokenResponse { + + /** 用户 ID */ + private Long userId; + + /** 令牌 */ + private String token; + + /** 过期于 */ + private long expiredAt; +} diff --git a/src/main/java/com/imyeyu/server/modules/minecraft/vo/server/ReportRequest.java b/src/main/java/com/imyeyu/server/modules/minecraft/vo/server/ReportRequest.java new file mode 100644 index 0000000..5cd2d9e --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/minecraft/vo/server/ReportRequest.java @@ -0,0 +1,14 @@ +package com.imyeyu.server.modules.minecraft.vo.server; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.server.modules.forevermc.bean.ServerStatus; + +/** + * @author 夜雨 + * @since 2024-08-06 16:36 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ReportRequest extends ServerStatus { +} diff --git a/src/main/java/com/imyeyu/server/modules/mirror/AbstractMirror.java b/src/main/java/com/imyeyu/server/modules/mirror/AbstractMirror.java new file mode 100644 index 0000000..59fda28 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/mirror/AbstractMirror.java @@ -0,0 +1,158 @@ +package com.imyeyu.server.modules.mirror; + +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.TimiServerAPI; +import com.imyeyu.server.config.dbsource.TimiServerDBConfig; +import com.imyeyu.server.modules.mirror.entity.Mirror; +import com.imyeyu.server.modules.mirror.service.MirrorService; +import com.imyeyu.utils.Time; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.core5.http.HttpHost; +import org.springframework.transaction.annotation.Transactional; + +/** + * 抽象镜像同步 + * + * @author 夜雨 + * @version 2024-05-23 14:21 + */ +@Slf4j +abstract class AbstractMirror { + + /** + * 镜像同步状态 + * + * @author 夜雨 + * @version 2024-05-23 14:27 + */ + public enum Status { + + /** 空闲 */ + IDLE, + + /** 正在同步 */ + SYNCING, + + /** 成功 */ + SUCCESSFUL, + + /** 同步失败 */ + FAIL + } + + /** 上一次同步时间 */ + long lastSyncAt = -1; + + /** 同步状态 */ + Status status = Status.IDLE; + + HttpHost proxy = new HttpHost("127.0.0.1", 10809); + + /** + * 初始化 + * + * @param mirror 镜像 + */ + final void initialize(Mirror mirror) { + status = Status.IDLE; + } + + /** @return true 为已初始化 */ + final boolean isInitialized() { + return status != null; + } + + /** + * 触发同步,由 {@link MirrorSyncTask} 调用 + * + * @param mirror 镜像 + */ + @Transactional(TimiServerDBConfig.ROLLBACKER) + final void sync0(Mirror mirror) { + long startAt = lastSyncAt = Time.now(); + status = Status.SYNCING; + try { + // 预备同步 + beforeSync(mirror); + + // 同步 + sync(mirror); + + // 成功 + status = Status.SUCCESSFUL; + MirrorService service = TimiServerAPI.applicationContext.getBean(MirrorService.class); + Mirror dbMirror = service.get(mirror.getId()); + dbMirror.setLastSyncAt(Time.now()); + service.update(dbMirror); + + // 同步成功 + status = Status.SUCCESSFUL; + onSuccessful(mirror); + } catch (Exception e) { + status = Status.FAIL; + + onException(mirror, e); + + if (e instanceof TimiException te) { + log.warn("[%s] Fail: %s".formatted(mirror.getBean(), te.getMsg())); + } else { + log.error("[%s] Error".formatted(mirror.getBean()), e); + } + } finally { + onFinally(mirror); + + String usedTime = Time.Media.toString(Time.now() - startAt); + log.info("[{}] synced {} in {}", mirror.getBean(), status, usedTime); + + if (status == Status.SUCCESSFUL) { + status = Status.IDLE; + } + } + } + + /** + * 预备同步 + * + * @param mirror 镜像 + * @throws Exception 准备异常 + */ + protected void beforeSync(Mirror mirror) throws Exception { + // 子类实现 + } + + /** + * 同步,子类实现 + * + * @param mirror 镜像 + * @throws Exception 同步异常 + */ + protected abstract void sync(Mirror mirror) throws Exception; + + /** + * 同步成功 + * + * @param mirror 镜像 + */ + protected void onSuccessful(Mirror mirror) { + // 子类实现 + } + + /** + * 同步失败 + * + * @param mirror 镜像 + * @param e 异常 + */ + protected void onException(Mirror mirror, Exception e) { + // 子类实现 + } + + /** + * 最终执行,触发同步后无论同步结果如何都触发此方法 + * + * @param mirror 镜像 + */ + protected void onFinally(Mirror mirror) { + // 子类实现 + } +} diff --git a/src/main/java/com/imyeyu/server/modules/mirror/AttachmentMirror.java b/src/main/java/com/imyeyu/server/modules/mirror/AttachmentMirror.java new file mode 100644 index 0000000..6c9a9f9 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/mirror/AttachmentMirror.java @@ -0,0 +1,88 @@ +package com.imyeyu.server.modules.mirror; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.server.TimiServerAPI; +import com.imyeyu.server.modules.common.entity.Attachment; +import com.imyeyu.server.modules.common.service.AttachmentService; +import com.imyeyu.server.modules.common.vo.attachment.AttachmentRequest; +import com.imyeyu.server.modules.mirror.entity.Mirror; + +import java.util.List; + +/** + * 附件镜像,镜像同步器储存镜像文件时使用此抽象镜像 + * + * @author 夜雨 + * @version 2024-06-07 23:35 + */ +@Slf4j +@Getter +public abstract class AttachmentMirror extends AbstractMirror { + + /** 同步结果:新增数量 */ + protected int syncAdded = 0; + + /** 同步结果:移除数量 */ + protected int syncRemoved = 0; + + /** 为 true 时同步产生新增或移除更新 */ + protected boolean hasUpdated = false; + + @Override + protected void beforeSync(Mirror mirror) throws Exception { + syncAdded = syncRemoved = 0; + hasUpdated = false; + } + + /** + * 差分新增附件 + * + * @param dbFiles 当前数据库附件 + * @return 新增附件 + * @throws Exception 处理异常 + */ + protected abstract List diffAdd(List dbFiles) throws Exception; + + /** + * 差分移除附件 + * + * @param dbFiles 当前数据库附件 + * @return 移除附件 + * @throws Exception 处理异常 + */ + protected abstract List diffRemove(List dbFiles) throws Exception; + + /** + * 同步,子类复现务必继续调用父类方法,保留 super.sync(mirror),否则附件差分同步无效 + * + * @param mirror 镜像 + * @throws Exception 同步异常 + */ + @Override + protected void sync(Mirror mirror) throws Exception { + AttachmentService attachmentService = TimiServerAPI.applicationContext.getBean(AttachmentService.class); + List dbFiles = attachmentService.listByBizId(Attachment.BizType.MIRROR, mirror.getId()); + + List diffRemoveList = diffRemove(dbFiles); + syncRemoved = diffRemoveList.size(); + for (int i = 0; i < diffRemoveList.size(); i++) { + attachmentService.destroy(diffRemoveList.get(i).getId()); + } + + List diffAddList = diffAdd(dbFiles); + syncAdded = diffAddList.size(); + for (int i = 0; i < diffAddList.size(); i++) { + AttachmentRequest request = diffAddList.get(i); + request.setBizType(Attachment.BizType.MIRROR); + request.setBizId(mirror.getId()); + attachmentService.create(request); + } + hasUpdated = syncAdded != 0 || syncRemoved != 0; + } + + @Override + protected void onSuccessful(Mirror mirror) { + log.info("[{}] attachment synced, added: {}, removed: {}", mirror.getBean(), syncAdded, syncRemoved); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/mirror/FabricAPIMirror.java b/src/main/java/com/imyeyu/server/modules/mirror/FabricAPIMirror.java new file mode 100644 index 0000000..dad3bdf --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/mirror/FabricAPIMirror.java @@ -0,0 +1,152 @@ +package com.imyeyu.server.modules.mirror; + +import com.google.gson.Gson; +import com.imyeyu.io.IO; +import com.imyeyu.server.modules.common.entity.Attachment; +import com.imyeyu.server.modules.common.service.AttachmentService; +import com.imyeyu.server.modules.common.vo.attachment.AttachmentRequest; +import com.imyeyu.server.modules.mirror.bean.AttachType; +import com.imyeyu.server.modules.mirror.data.FabricAPI; +import com.imyeyu.server.modules.mirror.entity.Mirror; +import com.imyeyu.server.modules.mirror.service.MirrorService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.client5.http.fluent.Request; +import org.dom4j.Document; +import org.dom4j.Element; +import org.dom4j.io.SAXReader; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Minecraft FabricApi 模组镜像 + * + * @author 夜雨 + * @version 2024-05-23 14:41 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class FabricAPIMirror extends AttachmentMirror { + + /** 主域名 */ + private static final String FABRIC_DOMAIN = "https://maven.fabricmc.net/"; + + /** 模组列表 */ + private static final String API_LIST = FABRIC_DOMAIN + "/net/fabricmc/fabric-api/fabric-api/maven-metadata.xml"; + + /** 下载地址 */ + private static final String API_DOWNLOAD = "https://github.com/FabricMC/fabric/releases/download/%s/fabric-api-%s.jar"; + + /** 版本匹配正则 */ + private static final Pattern versionRegex = Pattern.compile("^(\\d+\\.){1,2}(\\*|\\d+)$"); + + private final Gson gson; + + private final MirrorService service; + private final AttachmentService attachmentService; + + /** 游戏版本: Fabric 版本 */ + private final Map versionMap = new HashMap<>(); + + @Override + protected void beforeSync(Mirror mirror) throws Exception { + super.beforeSync(mirror); + + Document dom = new SAXReader().read(API_LIST); + Element root = dom.getRootElement(); + Element versioning = root.element("versioning"); + Element versions = versioning.element("versions"); + List versionList = versions.elements("version"); + + versionMap.clear(); + for (int i = 0; i < versionList.size(); i++) { + if (versionList.get(i) instanceof Element el) { + String[] args = el.getTextTrim().split("\\+"); + if (versionRegex.matcher(args[1]).matches()) { + versionMap.put(args[1], args[0]); + } + } + } + } + + @Override + protected void sync(Mirror mirror) throws Exception { + super.sync(mirror); + + if (!hasUpdated) { + return; + } + List result = new ArrayList<>(); + { + List attachmentList = attachmentService.listByBizId(Attachment.BizType.MIRROR, mirror.getId(), AttachType.FABRIC_API); + for (int i = 0; i < attachmentList.size(); i++) { + Attachment attachment = attachmentList.get(i); + // 附件名:fabric-api-(fabricVer)+(minecraftVer).jar + String[] args = attachment.getName().split("-")[2].replaceAll("\\.jar", "").split("\\+"); + + FabricAPI item = new FabricAPI(); + item.setName(attachment.getName()); + item.setFabricVer(args[0]); + item.setMinecraftVer(args[1]); + item.setMongoId(attachment.getMongoId()); + + result.add(item); + } + } + mirror.setData(gson.toJsonTree(result)); + service.update(mirror); + } + + @Override + protected List diffAdd(List dbFiles) throws Exception { + Set dbNameSet = dbFiles.stream().map(Attachment::getName).collect(Collectors.toSet()); + + List result = new ArrayList<>(); + for (Map.Entry item : versionMap.entrySet()) { + String version = "%s+%s".formatted(item.getValue(), item.getKey()); + String name = "fabric-api-%s.jar".formatted(version); + if (!dbNameSet.contains(name)) { + String url = API_DOWNLOAD.formatted(version, version); + log.info("Syncing a new fabric-api from {}", url); + + byte[] bytes = Request.get(url).viaProxy(proxy).execute().returnContent().asBytes(); + + AttachmentRequest attachment = new AttachmentRequest(); + attachment.setAttachTypeValue(AttachType.FABRIC_API); + attachment.setName(name); + attachment.setSize((long) bytes.length); + attachment.setInputStream(new ByteArrayInputStream(bytes)); + result.add(attachment); + } + } + return result; + } + + @Override + protected List diffRemove(List dbFiles) { + List versionList = new ArrayList<>(); + for (Map.Entry item : versionMap.entrySet()) { + String version = "%s+%s".formatted(item.getValue(), item.getKey()); + versionList.add("fabric-api-%s.jar".formatted(version)); + } + List result = new ArrayList<>(); + for (int i = 0; i < dbFiles.size(); i++) { + Attachment attachment = dbFiles.get(i); + if (!versionList.contains(attachment.getName())) { + log.info("Syncing a miss fabric-api for {}", attachment.getName()); + result.add(attachment); + } + } + return result; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/mirror/MirrorSyncTask.java b/src/main/java/com/imyeyu/server/modules/mirror/MirrorSyncTask.java new file mode 100644 index 0000000..5416776 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/mirror/MirrorSyncTask.java @@ -0,0 +1,70 @@ +package com.imyeyu.server.modules.mirror; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.java.TimiJava; +import com.imyeyu.server.TimiServerAPI; +import com.imyeyu.server.modules.mirror.entity.Mirror; +import com.imyeyu.server.modules.mirror.service.MirrorService; +import com.imyeyu.utils.Time; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.scheduling.support.CronTrigger; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 镜像同步任务触发器 + *

触发器周期为每分钟轮询一次,镜像同步周期由 {@link Mirror#setPeriod(int)} 决定 + * + * @author 夜雨 + * @version 2024-05-23 14:22 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class MirrorSyncTask implements SchedulingConfigurer, TimiJava { + + private final MirrorService service; + private final ThreadPoolTaskExecutor threadPoolTaskExecutor; + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + taskRegistrar.addTriggerTask(() -> { + List mirrors = service.listAll(); + long now = Time.now(); + for (int i = 0; i < mirrors.size(); i++) { + Mirror dbMirror = mirrors.get(i); + if (!dbMirror.isEnable()) { + continue; + } + try { + Object bean = TimiServerAPI.applicationContext.getBean(Class.forName(dbMirror.getBean())); + if (bean instanceof AbstractMirror mirror) { + if (!mirror.isInitialized()) { + mirror.initialize(dbMirror); + } + if (now - mirror.lastSyncAt < dbMirror.getPeriod() * Time.M) { + continue; + } + switch (mirror.status) { + case IDLE -> { + log.info("[{}] Starting..", dbMirror.getBean()); + threadPoolTaskExecutor.execute(() -> mirror.sync0(dbMirror)); + } + case SYNCING -> log.warn("[%s] is syncing, skipped in this period. This task maybe need more time to finish work"); + case FAIL -> { + log.info("[{}] Retrying..", dbMirror.getBean()); + threadPoolTaskExecutor.execute(() -> mirror.sync0(dbMirror)); + } + } + } + } catch (ClassNotFoundException e) { + log.error("[%s] Initialization fail".formatted(dbMirror.getBean()), e); + } + } + }, tc -> new CronTrigger("0 * * * * ?").nextExecution(tc)); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/mirror/OpenJDKGithubMirror.java b/src/main/java/com/imyeyu/server/modules/mirror/OpenJDKGithubMirror.java new file mode 100644 index 0000000..fa10c86 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/mirror/OpenJDKGithubMirror.java @@ -0,0 +1,119 @@ +package com.imyeyu.server.modules.mirror; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.java.ref.Ref; +import com.imyeyu.server.modules.mirror.data.OpenJDK; +import com.imyeyu.server.modules.mirror.entity.Mirror; +import com.imyeyu.server.modules.mirror.service.MirrorService; +import com.imyeyu.utils.OS; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.client5.http.fluent.Request; +import org.apache.hc.core5.util.Timeout; +import org.springframework.stereotype.Service; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Github JDK 镜像,仅同步下载链接等信息,不储存文件 + * + * @author 夜雨 + * @version 2024-06-10 10:51 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class OpenJDKGithubMirror extends AbstractMirror { + + /** 版本发布列表接口,插值 {@link #REPOS_MAP} 的值 */ + private static final String API_RELEASE = "https://api.github.com/repos/adoptium/%s/releases?page=1"; + + /** 版本仓库映射,key 为版本,value 为对应仓库,只读 */ + private static final Map REPOS_MAP = Collections.unmodifiableMap(new HashMap<>() {{ + put("8", "temurin8-binaries"); + put("11", "temurin11-binaries"); + put("17", "temurin17-binaries"); + put("21", "temurin21-binaries"); + }}); + + private final Gson gson; + private final MirrorService service; + + @Override + protected void sync(Mirror mirror) throws Exception { + mirror.setData(gson.toJsonTree(fetch())); + service.update(mirror); + } + + /** + * 获取 {@link OpenJDK} 列表,{@link OpenJDKMirror} 也会使用此接口 + * + * @return jdk 列表 + * @throws Exception 获取异常 + */ + final List fetch() throws Exception { + List result = new ArrayList<>(); + for (Map.Entry repo : REPOS_MAP.entrySet()) { + String respText = Request.get(API_RELEASE.formatted(repo.getValue())).connectTimeout(Timeout.ofSeconds(60)).execute().returnContent().asString(); + JsonArray root = JsonParser.parseString(respText).getAsJsonArray(); + JsonObject itemRel = null; + for (JsonElement el : root) { + itemRel = el.getAsJsonObject(); + if (itemRel.get("prerelease").getAsBoolean() || itemRel.get("draft").getAsBoolean()) { + // 忽略草稿或预发布版本 + itemRel = null; + } else { + break; + } + } + if (itemRel == null) { + throw new TimiException(TimiCode.ERROR, "not found release item for " + repo.getValue()); + } + JsonArray assets = itemRel.get("assets").getAsJsonArray(); + for (JsonElement asset : assets) { + JsonObject itemAsset = asset.getAsJsonObject(); + // OpenJDK21U-jdk_x64_windows_hotspot_21.0.3_9.zip + String name = itemAsset.get("name").getAsString(); + + if (!name.contains("-") || !name.contains("_")) { + continue; + } + if (!name.contains("x64")) { + continue; + } + if (!name.endsWith(".zip") && !name.endsWith(".tar.gz")) { + continue; + } + String[] split = name.split("-")[1].split("_"); + if (split.length < 3) { + continue; + } + OS.Platform platform = Ref.toType(OS.Platform.class, split[2].toUpperCase()); + OpenJDK.Type type = Ref.toType(OpenJDK.Type.class, split[0].toUpperCase()); + + if (platform != null && type != null) { + OpenJDK jdk = new OpenJDK(); + jdk.setPlatform(platform); + jdk.setType(type); + jdk.setName(name); + jdk.setVersion(repo.getKey()); + jdk.setData(URLDecoder.decode(itemAsset.get("browser_download_url").getAsString(), StandardCharsets.UTF_8)); + result.add(jdk); + } + } + } + return result; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/mirror/OpenJDKMirror.java b/src/main/java/com/imyeyu/server/modules/mirror/OpenJDKMirror.java new file mode 100644 index 0000000..144bcc5 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/mirror/OpenJDKMirror.java @@ -0,0 +1,112 @@ +package com.imyeyu.server.modules.mirror; + +import com.google.gson.Gson; +import com.imyeyu.io.IO; +import com.imyeyu.server.modules.common.entity.Attachment; +import com.imyeyu.server.modules.common.service.AttachmentService; +import com.imyeyu.server.modules.common.vo.attachment.AttachmentRequest; +import com.imyeyu.server.modules.mirror.bean.AttachType; +import com.imyeyu.server.modules.mirror.data.OpenJDK; +import com.imyeyu.server.modules.mirror.entity.Mirror; +import com.imyeyu.server.modules.mirror.service.MirrorService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.client5.http.fluent.Request; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 本地 JDK 镜像,使用 {@link OpenJDKGithubMirror} 镜像同步 + * + * @author 夜雨 + * @version 2024-06-11 10:15 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class OpenJDKMirror extends AttachmentMirror { + + private final Gson gson; + private final MirrorService service; + private final AttachmentService attachmentService; + private final OpenJDKGithubMirror githubMirror; + + /** Github 镜像结果 */ + private List githubMirrorResult; + + @Override + protected void beforeSync(Mirror mirror) throws Exception { + super.beforeSync(mirror); + + // 本地镜像来自 Github + githubMirrorResult = githubMirror.fetch(); + } + + @Override + protected void sync(Mirror mirror) throws Exception { + super.sync(mirror); + + if (!hasUpdated) { + return; + } + Map githubNameMap = githubMirrorResult.stream().collect(Collectors.toMap(OpenJDK::getName, item -> item)); + List result = new ArrayList<>(); + { + List attachmentList = attachmentService.listByBizId(Attachment.BizType.MIRROR, mirror.getId(), AttachType.OPEN_JDK); + for (int i = 0; i < attachmentList.size(); i++) { + Attachment attachment = attachmentList.get(i); + OpenJDK jdk = githubNameMap.get(attachment.getName()); + jdk.setData(attachment.getMongoId()); + result.add(jdk); + } + } + mirror.setData(gson.toJsonTree(result)); + service.update(mirror); + } + + @Override + protected List diffAdd(List dbFiles) throws Exception { + Map githubNameMap = githubMirrorResult.stream().collect(Collectors.toMap(OpenJDK::getName, item -> item)); + Set dbNameSet = dbFiles.stream().map(Attachment::getName).collect(Collectors.toSet()); + + List result = new ArrayList<>(); + for (Map.Entry item : githubNameMap.entrySet()) { + if (!dbNameSet.contains(item.getKey())) { + String url = item.getValue().getData(); + log.info("Syncing a new open-jdk from {}", url); + + byte[] bytes = Request.get(url).viaProxy(proxy).execute().returnContent().asBytes(); + + AttachmentRequest attachment = new AttachmentRequest(); + attachment.setAttachTypeValue(AttachType.OPEN_JDK); + attachment.setName(item.getKey()); + attachment.setSize((long) bytes.length); + attachment.setInputStream(new ByteArrayInputStream(bytes)); + result.add(attachment); + } + } + return result; + } + + @Override + protected List diffRemove(List dbFiles) { + Set githubNameSet = githubMirrorResult.stream().map(OpenJDK::getName).collect(Collectors.toSet()); + + List result = new ArrayList<>(); + for (int i = 0; i < dbFiles.size(); i++) { + Attachment attachment = dbFiles.get(i); + if (!githubNameSet.contains(attachment.getName())) { + log.info("Syncing a miss open-jdk for {}", attachment.getName()); + result.add(attachment); + } + } + return result; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/mirror/OpenJDKTunaMirror.java b/src/main/java/com/imyeyu/server/modules/mirror/OpenJDKTunaMirror.java new file mode 100644 index 0000000..f979925 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/mirror/OpenJDKTunaMirror.java @@ -0,0 +1,83 @@ +package com.imyeyu.server.modules.mirror; + +import com.google.gson.Gson; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.server.modules.mirror.data.OpenJDK; +import com.imyeyu.server.modules.mirror.entity.Mirror; +import com.imyeyu.server.modules.mirror.service.MirrorService; +import com.imyeyu.utils.OS; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +/** + * OpenJDK 清华大学源镜像,仅同步下载链接,不储存文件 + * + * @author 夜雨 + * @version 2024-06-11 10:48 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class OpenJDKTunaMirror extends AbstractMirror { + + /** + * 页面地址模板,插值参数(小写) + *

    + *
  1. {@link #VERSIONS}
  2. + *
  3. {@link OpenJDK.Type}
  4. + *
  5. {@link com.imyeyu.utils.OS.Platform}
  6. + *
+ * + */ + private static final String PAGE_URL_TEMPLATE = "https://mirrors.tuna.tsinghua.edu.cn/Adoptium/%s/%s/x64/%s/"; + + /** 版本列表 */ + private static final String[] VERSIONS = {"8", "11", "17", "21"}; + + private final Gson gson; + private final MirrorService service; + + @Override + protected void sync(Mirror mirror) throws Exception { + List result = new ArrayList<>(); + for (int i = 0; i < VERSIONS.length; i++) { + OpenJDK.Type[] types = OpenJDK.Type.values(); + for (int j = 0; j < types.length; j++) { + OS.Platform[] platforms = OS.Platform.values(); + for (int k = 0; k < platforms.length; k++) { + String url = PAGE_URL_TEMPLATE.formatted(VERSIONS[i], types[j].toString().toLowerCase(), platforms[k].toString().toLowerCase()); + Document document = Jsoup.connect(url).get(); + Element fileList = document.getElementById("list"); + Elements linkTDList = fileList.getElementsByClass("link"); + for (Element element : linkTDList) { + Elements linkA = element.getElementsByTag("a"); + for (Element a : linkA) { + String href = a.attr("href"); + if (!href.endsWith(".zip") && !href.endsWith(".tar.gz")) { + continue; + } + OpenJDK jdk = new OpenJDK(); + jdk.setPlatform(platforms[k]); + jdk.setType(types[j]); + jdk.setName(href); + jdk.setVersion(VERSIONS[i]); + jdk.setData(url + href); + + result.add(jdk); + break; + } + } + } + } + } + mirror.setData(gson.toJsonTree(result)); + service.update(mirror); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/mirror/bean/AttachType.java b/src/main/java/com/imyeyu/server/modules/mirror/bean/AttachType.java new file mode 100644 index 0000000..6afe015 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/mirror/bean/AttachType.java @@ -0,0 +1,12 @@ +package com.imyeyu.server.modules.mirror.bean; + +/** + * @author 夜雨 + * @version 2024-05-23 16:54 + */ +public enum AttachType { + + FABRIC_API, + + OPEN_JDK, +} diff --git a/src/main/java/com/imyeyu/server/modules/mirror/controller/MirrorController.java b/src/main/java/com/imyeyu/server/modules/mirror/controller/MirrorController.java new file mode 100644 index 0000000..2b88f4f --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/mirror/controller/MirrorController.java @@ -0,0 +1,64 @@ +package com.imyeyu.server.modules.mirror.controller; + +import com.google.gson.JsonElement; +import com.imyeyu.server.modules.mirror.entity.Mirror; +import com.imyeyu.server.modules.mirror.service.MirrorService; +import com.imyeyu.server.modules.mirror.vo.MirrorView; +import com.imyeyu.spring.annotation.RequestRateLimit; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; + +/** + * 镜像接口 + * + * @author 夜雨 + * @version 2024-06-11 12:53 + */ +@Slf4j +@RestController +@RequestMapping("/mirror") +@RequiredArgsConstructor +public class MirrorController { + + private final MirrorService service; + + /** + * 获取镜像信息 + * + * @param mirrorName 镜像名称 + * @return 镜像信息 + */ + @RequestRateLimit + @GetMapping("/{mirrorName}") + public JsonElement get(@PathVariable String mirrorName) { + return service.getByName(mirrorName).getData(); + } + + /** + * 获取镜像列表 + * + * @return 镜像列表 + */ + @RequestRateLimit + @GetMapping("/list") + public List list() { + List result = new ArrayList<>(); + List mirrors = service.listAll(); + for (int i = 0; i < mirrors.size(); i++) { + MirrorView target = new MirrorView(); + BeanUtils.copyProperties(mirrors.get(i), target); + target.setBean(null); + target.setData(null); + result.add(target); + } + return result; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/mirror/data/FabricAPI.java b/src/main/java/com/imyeyu/server/modules/mirror/data/FabricAPI.java new file mode 100644 index 0000000..4046c1b --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/mirror/data/FabricAPI.java @@ -0,0 +1,26 @@ +package com.imyeyu.server.modules.mirror.data; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * FabricAPI + * + * @author 夜雨 + * @version 2024-06-08 00:03 + */ +@Data +public class FabricAPI { + + /** 名称 */ + private String name; + + /** FabricAPI 版本 */ + private String fabricVer; + + /** Minecraft 版本 */ + private String minecraftVer; + + /** 映射文件 mongoId */ + private String mongoId; +} diff --git a/src/main/java/com/imyeyu/server/modules/mirror/data/OpenJDK.java b/src/main/java/com/imyeyu/server/modules/mirror/data/OpenJDK.java new file mode 100644 index 0000000..f06c136 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/mirror/data/OpenJDK.java @@ -0,0 +1,44 @@ +package com.imyeyu.server.modules.mirror.data; + +import lombok.Data; +import com.imyeyu.utils.OS; + +/** + * OpenJDK + * + * @author 夜雨 + * @version 2024-06-10 10:35 + */ +@Data +public class OpenJDK { + + /** + * 类型 + * + * @author 夜雨 + * @version 2024-06-10 10:35 + */ + public enum Type { + + /** 集成开发工具 */ + JDK, + + /** 运行时 */ + JRE + } + + /** 类型 */ + private Type type; + + /** 平台 */ + private OS.Platform platform; + + /** 版本 */ + private String version; + + /** 名称 */ + private String name; + + /** 数据(可能是下载链接,可能是 mongoId) */ + private String data; +} diff --git a/src/main/java/com/imyeyu/server/modules/mirror/entity/Mirror.java b/src/main/java/com/imyeyu/server/modules/mirror/entity/Mirror.java new file mode 100644 index 0000000..8af8847 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/mirror/entity/Mirror.java @@ -0,0 +1,35 @@ +package com.imyeyu.server.modules.mirror.entity; + +import com.google.gson.JsonElement; +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.spring.entity.Entity; + +/** + * 镜像 + * + * @author 夜雨 + * @version 2024-05-23 15:22 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class Mirror extends Entity { + + /** 执行 JavaBean */ + protected String bean; + + /** 名称 */ + protected String name; + + /** 同步数据 */ + protected JsonElement data; + + /** 周期(分钟) */ + protected int period; + + /** 上一次同步时间 */ + protected Long lastSyncAt; + + /** true 为启用同步 */ + protected boolean isEnable; +} diff --git a/src/main/java/com/imyeyu/server/modules/mirror/mapper/MirrorMapper.java b/src/main/java/com/imyeyu/server/modules/mirror/mapper/MirrorMapper.java new file mode 100644 index 0000000..05de846 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/mirror/mapper/MirrorMapper.java @@ -0,0 +1,29 @@ +package com.imyeyu.server.modules.mirror.mapper; + +import com.imyeyu.server.modules.mirror.entity.Mirror; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +/** + * 镜像 + * + * @author 夜雨 + * @version 2024-05-23 15:21 + */ +public interface MirrorMapper extends BaseMapper { + + @Select("SELECT * FROM mirror WHERE id = #{id} LIMIT 1") + Mirror select(Long id); + + @Update("UPDATE mirror SET data = #{data}, last_sync_at = #{lastSyncAt} WHERE id = #{id} LIMIT 1") + void update(Mirror mirror); + + @Select("SELECT * FROM mirror WHERE name = #{name} LIMIT 1") + Mirror queryByName(String name); + + @Select("SELECT * FROM mirror WHERE deleted_at IS NULL") + List listAll(); +} diff --git a/src/main/java/com/imyeyu/server/modules/mirror/service/MirrorService.java b/src/main/java/com/imyeyu/server/modules/mirror/service/MirrorService.java new file mode 100644 index 0000000..ea74594 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/mirror/service/MirrorService.java @@ -0,0 +1,34 @@ +package com.imyeyu.server.modules.mirror.service; + +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.mirror.entity.Mirror; +import com.imyeyu.spring.service.GettableService; +import com.imyeyu.spring.service.UpdatableService; + +import java.util.List; + +/** + * 镜像服务 + * + * @author 夜雨 + * @version 2024-05-23 15:21 + */ +public interface MirrorService extends GettableService, UpdatableService { + + /** + * 根据名称获取 + * + * @param name 名称 + * @return 镜像 + * @throws TimiException 服务异常 + */ + Mirror getByName(String name) throws TimiException; + + /** + * 获取所有镜像 + * + * @return 镜像列表 + * @throws TimiException 服务异常 + */ + List listAll() throws TimiException; +} diff --git a/src/main/java/com/imyeyu/server/modules/mirror/service/implement/MirrorServiceImplement.java b/src/main/java/com/imyeyu/server/modules/mirror/service/implement/MirrorServiceImplement.java new file mode 100644 index 0000000..2858ff9 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/mirror/service/implement/MirrorServiceImplement.java @@ -0,0 +1,40 @@ +package com.imyeyu.server.modules.mirror.service.implement; + +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.mirror.entity.Mirror; +import com.imyeyu.server.modules.mirror.mapper.MirrorMapper; +import com.imyeyu.server.modules.mirror.service.MirrorService; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 镜像服务 + * + * @author 夜雨 + * @version 2024-05-23 15:22 + */ +@Service +@RequiredArgsConstructor +public class MirrorServiceImplement extends AbstractEntityService implements MirrorService { + + private final MirrorMapper mapper; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public Mirror getByName(String name) throws TimiException { + return mapper.queryByName(name); + } + + @Override + public List listAll() throws TimiException { + return mapper.listAll(); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/mirror/vo/MirrorView.java b/src/main/java/com/imyeyu/server/modules/mirror/vo/MirrorView.java new file mode 100644 index 0000000..7256c2e --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/mirror/vo/MirrorView.java @@ -0,0 +1,12 @@ +package com.imyeyu.server.modules.mirror.vo; + +import com.imyeyu.server.modules.mirror.entity.Mirror; + +/** + * 镜像视图 + * + * @author 夜雨 + * @version 2024-06-12 10:20 + */ +public class MirrorView extends Mirror { +} diff --git a/src/main/java/com/imyeyu/server/modules/music/bean/ChannelBinding.java b/src/main/java/com/imyeyu/server/modules/music/bean/ChannelBinding.java new file mode 100644 index 0000000..a39fe64 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/music/bean/ChannelBinding.java @@ -0,0 +1,28 @@ +package com.imyeyu.server.modules.music.bean; + +import io.netty.channel.ChannelHandlerContext; +import lombok.Data; +import com.imyeyu.utils.Time; + +/** + * 频道绑定 + * + * @author 夜雨 + * @version 2023-02-07 14:43 + */ +@Data +public class ChannelBinding { + + /** 与受控端连接频道 */ + private ChannelHandlerContext playerChannel; + + /** 与控制端连接频道 */ + private ChannelHandlerContext controllerChannel; + + private long lastActiviedAt = -1; + + /** @return true 为超过 1 分钟未发生通信 */ + public boolean isTimeout() { + return Time.M < Time.now() - lastActiviedAt; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/music/bean/pkg/BasePackage.java b/src/main/java/com/imyeyu/server/modules/music/bean/pkg/BasePackage.java new file mode 100644 index 0000000..10500b0 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/music/bean/pkg/BasePackage.java @@ -0,0 +1,46 @@ +package com.imyeyu.server.modules.music.bean.pkg; + +import lombok.Data; + +/** + * 基本数据包 + * + * @author 夜雨 + * @version 2023-02-08 11:14 + */ +@Data +abstract class BasePackage { + + /** + * 播放模式 + * + * @author 夜雨 + * @version 2023-02-08 14:12 + */ + public enum Type { + + /** 单曲循环 */ + REPEAT, + + /** 队列循环 */ + REPEAT_PLAY_LIST, + + /** 队后单循 */ + REPEAT_PLAY_LIST_END, + + /** 列表循环 */ + REPEAT_FILE_LIST + } + + /** 频道 */ + private String channelID; + + /** 播放模式 */ + private Type type; + + /** 音量 */ + private double volume; + + /** 包生成时间 */ + private Long createdAt; +} diff --git a/src/main/java/com/imyeyu/server/modules/music/bean/pkg/ControllerPackage.java b/src/main/java/com/imyeyu/server/modules/music/bean/pkg/ControllerPackage.java new file mode 100644 index 0000000..8c6bde2 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/music/bean/pkg/ControllerPackage.java @@ -0,0 +1,47 @@ +package com.imyeyu.server.modules.music.bean.pkg; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * 控制端数据包,由控制器发出至受控端 + * + * @author 夜雨 + * @version 2023-02-08 11:15 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class ControllerPackage extends BasePackage { + + /** + * @author 夜雨 + * @version 2023-02-07 11:37 + */ + public enum Action { + + /** 上一首 */ + PREVIOUS, + + /** 切换播放状态 */ + TOGGLE, + + /** 下一首 */ + NEXT, + + /** 随机列表 */ + RANDOM, + + /** 音量调整 */ + VOLUME, + + /** 播放模式调整 */ + TYPE + } + + /** 控制动作 */ + private Action action; +} diff --git a/src/main/java/com/imyeyu/server/modules/music/bean/pkg/PlayerPackage.java b/src/main/java/com/imyeyu/server/modules/music/bean/pkg/PlayerPackage.java new file mode 100644 index 0000000..49a2222 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/music/bean/pkg/PlayerPackage.java @@ -0,0 +1,21 @@ +package com.imyeyu.server.modules.music.bean.pkg; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 受控端数据包,推送播放器状态到控制器 + * + * @author 夜雨 + * @version 2023-02-08 11:15 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class PlayerPackage extends BasePackage { + + /** 当前播放 */ + private String nowPlaying; + + /** 下一首播放 */ + private String nextPlay; +} diff --git a/src/main/java/com/imyeyu/server/modules/music/core/Middleware.java b/src/main/java/com/imyeyu/server/modules/music/core/Middleware.java new file mode 100644 index 0000000..f547f8b --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/music/core/Middleware.java @@ -0,0 +1,100 @@ +package com.imyeyu.server.modules.music.core; + +import com.google.gson.Gson; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.utils.Time; +import com.imyeyu.java.TimiJava; +import com.imyeyu.server.modules.music.bean.ChannelBinding; +import com.imyeyu.server.modules.music.bean.pkg.ControllerPackage; +import com.imyeyu.server.modules.music.bean.pkg.PlayerPackage; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.scheduling.support.CronTrigger; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +/** + * 中转站 + * + * @author 夜雨 + * @version 2021-11-03 22:56 + */ +@Slf4j +@Component +public class Middleware implements SchedulingConfigurer { + + /** 缓存清理间隔 */ + private static final String CACHE_CLEAR_CORN = "0 0/20 * * * ?"; + + @Autowired + private Gson gson; + + /** 缓存频道,Map<频道 ID, 关联频道> */ + private final Map channels = new HashMap<>(); + + @Override + public void configureTasks(ScheduledTaskRegistrar registrar) { + // 定时清理过期频道 + Trigger trigger = tc -> new CronTrigger(CACHE_CLEAR_CORN).nextExecution(tc); + registrar.addTriggerTask(() -> channels.keySet().removeIf(channelID -> channels.get(channelID).isTimeout()), trigger); + } + + /** + * 推送受控端数据 + * + * @param ctx 受控端频道(关联控制端) + * @param pkg 数据包 + */ + public void pushPlayerData(ChannelHandlerContext ctx, PlayerPackage pkg) { + if (TimiJava.isEmpty(pkg.getChannelID())) { + return; + } + ChannelBinding channelBinding = channels.get(pkg.getChannelID()); + if (channelBinding == null) { + channelBinding = new ChannelBinding(); + channels.put(pkg.getChannelID(), channelBinding); + log.info("TimiMusic created channel: " + pkg.getChannelID()); + } + channelBinding.setPlayerChannel(ctx); + channelBinding.setLastActiviedAt(Time.now()); + if (channelBinding.getControllerChannel() != null) { + channelBinding.getControllerChannel().writeAndFlush(new TextWebSocketFrame(gson.toJson(pkg))); + } + } + + /** + * 推送控制端数据 + * + * @param ctx 控制端频道(关联受控端) + * @param pkg 数据包 + */ + public void pushControllerData(ChannelHandlerContext ctx, ControllerPackage pkg) { + if (TimiJava.isEmpty(pkg.getChannelID())) { + return; + } + ChannelBinding channelBinding = channels.get(pkg.getChannelID()); + if (channelBinding == null) { + channelBinding = new ChannelBinding(); + channels.put(pkg.getChannelID(), channelBinding); + log.info("TimiMusic created channel: " + pkg.getChannelID()); + } + channelBinding.setControllerChannel(ctx); + channelBinding.setLastActiviedAt(Time.now()); + if (pkg.getAction() != null && channelBinding.getPlayerChannel() != null) { + byte[] bytes = gson.toJson(pkg).getBytes(StandardCharsets.UTF_8); + ByteBuf buffer = Unpooled.buffer(); + buffer.writeInt(bytes.length); + buffer.writeBytes(bytes); + channelBinding.getPlayerChannel().writeAndFlush(buffer); + } + } +} diff --git a/src/main/java/com/imyeyu/server/modules/music/handler/ControllerMessageHandler.java b/src/main/java/com/imyeyu/server/modules/music/handler/ControllerMessageHandler.java new file mode 100644 index 0000000..2c551f0 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/music/handler/ControllerMessageHandler.java @@ -0,0 +1,52 @@ +package com.imyeyu.server.modules.music.handler; + +import com.google.gson.Gson; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.server.modules.music.bean.pkg.ControllerPackage; +import com.imyeyu.server.modules.music.core.Middleware; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 控制端消息处理 + * + * @author 夜雨 + * @version 2021-11-03 15:34 + */ +@Slf4j +@ChannelHandler.Sharable +@Component +public class ControllerMessageHandler extends SimpleChannelInboundHandler { + + @Autowired + private Gson gson; + + @Autowired + private Middleware middleware; + + @Override + protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame msg) { + if (msg instanceof TextWebSocketFrame textWebSocketFrame) { + middleware.pushControllerData(ctx, gson.fromJson(textWebSocketFrame.text(), ControllerPackage.class)); + } else { + ctx.channel().writeAndFlush(WebSocketCloseStatus.INVALID_MESSAGE_TYPE).addListener(ChannelFutureListener.CLOSE); + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + log.info("TimiMusic controller disconnected: " + ctx.channel().remoteAddress()); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + log.info("TimiMusic controller connected: " + ctx.channel().remoteAddress()); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/music/handler/PlayerMessageHandler.java b/src/main/java/com/imyeyu/server/modules/music/handler/PlayerMessageHandler.java new file mode 100644 index 0000000..08de598 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/music/handler/PlayerMessageHandler.java @@ -0,0 +1,44 @@ +package com.imyeyu.server.modules.music.handler; + +import com.google.gson.Gson; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.server.modules.music.bean.pkg.PlayerPackage; +import com.imyeyu.server.modules.music.core.Middleware; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 受控端消息处理 + * + * @author 夜雨 + * @version 2021-11-03 17:50 + */ +@Slf4j +@ChannelHandler.Sharable +@Component +public class PlayerMessageHandler extends SimpleChannelInboundHandler { + + @Autowired + private Gson gson; + + @Autowired + private Middleware middleware; + + @Override + protected synchronized void channelRead0(ChannelHandlerContext ctx, String result) { + middleware.pushPlayerData(ctx, gson.fromJson(result, PlayerPackage.class)); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + log.info("TimiMusic player disconnected: " + ctx.channel().remoteAddress()); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + log.info("TimiMusic player connected: " + ctx.channel().remoteAddress()); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/music/runner/ControllerBootstrapRunner.java b/src/main/java/com/imyeyu/server/modules/music/runner/ControllerBootstrapRunner.java new file mode 100644 index 0000000..a1ce516 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/music/runner/ControllerBootstrapRunner.java @@ -0,0 +1,137 @@ +package com.imyeyu.server.modules.music.runner; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; +import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler; +import io.netty.handler.stream.ChunkedWriteHandler; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.server.config.CORSConfig; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.server.modules.music.handler.ControllerMessageHandler; +import org.springframework.beans.BeansException; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.stereotype.Component; + +import java.net.InetSocketAddress; + +/** + * 控制端 Netty 服务 + * + * @author 夜雨 + * @version 2021-11-03 15:29 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ControllerBootstrapRunner implements ApplicationRunner, ApplicationListener, ApplicationContextAware { + + private final CORSConfig corsConfig; + private final SettingService settingService; + + private ApplicationContext applicationContext; + + private Channel channel; + private EventLoopGroup boss, worker; + + public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + public void run(ApplicationArguments args) { + final DefaultFullHttpResponse NFP = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND); + final DefaultFullHttpResponse BRP = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST); + + boss = new NioEventLoopGroup(); + worker = new NioEventLoopGroup(); + try { + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.group(boss, worker); + bootstrap.channel(NioServerSocketChannel.class); + bootstrap.localAddress(new InetSocketAddress(settingService.getAsInt(SettingKey.MUSIC_CONTROLLER_PORT))); + bootstrap.childHandler(new ChannelInitializer() { + + @Override + protected void initChannel(SocketChannel channel) { + ChannelPipeline pipeline = channel.pipeline(); + pipeline.addLast(new HttpServerCodec()); + pipeline.addLast(new ChunkedWriteHandler()); + pipeline.addLast(new HttpObjectAggregator(65536)); + pipeline.addLast(new ChannelInboundHandlerAdapter() { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object obj) throws Exception { + if (obj instanceof FullHttpRequest req) { + if (!req.uri().equals(settingService.getAsString(SettingKey.MUSIC_CONTROLLER_URI))) { + // 访问的路径不是 Web Socket 的端点地址,响应 404 + ctx.channel().writeAndFlush(NFP).addListener(ChannelFutureListener.CLOSE); + return; + } + // 域名限制 + String reqOrigin = req.headers().get(HttpHeaderNames.ORIGIN); + String[] allowOrigin = corsConfig.getAllowOrigin(); + for (int i = 0; i < allowOrigin.length; i++) { + if (allowOrigin[i].equals(reqOrigin)) { + super.channelRead(ctx, obj); + return; + } + } + ctx.channel().writeAndFlush(BRP).addListener(ChannelFutureListener.CLOSE); + return; + } + super.channelRead(ctx, obj); + } + }); + pipeline.addLast(new WebSocketServerCompressionHandler()); + pipeline.addLast(new WebSocketServerProtocolHandler( + settingService.getAsString(SettingKey.MUSIC_CONTROLLER_URI), + null, + true, + settingService.getAsInt(SettingKey.MUSIC_MAX_FRAME_LENGTH) + ) + ); + + // 从 IOC 获取 Handler + pipeline.addLast(applicationContext.getBean(ControllerMessageHandler.class)); + } + }); + channel = bootstrap.bind().sync().channel(); + log.info("TimiMusicRC controller service startup with " + settingService.getAsString(SettingKey.MUSIC_CONTROLLER_IP)); + } catch (Exception e) { + log.error("TimiMusicRC controller service startup error", e); + } + } + + public void onApplicationEvent(@NonNull ContextClosedEvent event) { + if (channel != null) { + channel.closeFuture(); + boss.shutdownGracefully(); + worker.shutdownGracefully(); + } + log.info("TimiMusicRC controller service shutdown finished"); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/music/runner/PlayerBootstrapRunner.java b/src/main/java/com/imyeyu/server/modules/music/runner/PlayerBootstrapRunner.java new file mode 100644 index 0000000..8d872c3 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/music/runner/PlayerBootstrapRunner.java @@ -0,0 +1,94 @@ +package com.imyeyu.server.modules.music.runner; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.netty.handler.codec.string.StringDecoder; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.server.modules.music.handler.PlayerMessageHandler; +import org.springframework.beans.BeansException; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.stereotype.Component; + +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +/** + * 受控端 Netty 服务 + * + * @author 夜雨 + * @version 2021-11-03 19:18 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class PlayerBootstrapRunner implements ApplicationRunner, ApplicationListener, ApplicationContextAware { + + private final SettingService settingService; + + private ApplicationContext applicationContext; + + private Channel channel; + private EventLoopGroup boss, worker; + + public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + public void run(ApplicationArguments args) { + ServerBootstrap bootstrap = new ServerBootstrap(); + boss = new NioEventLoopGroup(); + worker = new NioEventLoopGroup(); + try { + bootstrap.group(boss, worker); + bootstrap.channel(NioServerSocketChannel.class); + bootstrap.localAddress(new InetSocketAddress(settingService.getAsInt(SettingKey.MUSIC_PLAYER_PORT))); + bootstrap.childHandler(new ChannelInitializer<>() { + + @Override + protected void initChannel(Channel ch) { + ch.pipeline().addLast(new LengthFieldBasedFrameDecoder( + settingService.getAsInt(SettingKey.MUSIC_MAX_FRAME_LENGTH), + 0, + 4, + 0, + 4)); + ch.pipeline().addLast(new StringDecoder(StandardCharsets.UTF_8)); + ch.pipeline().addLast(applicationContext.getBean(PlayerMessageHandler.class)); + } + }); + bootstrap.option(ChannelOption.SO_BACKLOG, 1024); + bootstrap.childOption(ChannelOption.TCP_NODELAY, true); + bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true); + + channel = bootstrap.bind().sync().channel(); + + log.info("TimiMusicRC player service startup with " + settingService.getAsString(SettingKey.MUSIC_PLAYER_IP)); + } catch (Exception e) { + log.error("TimiMusicRC player service startup error", e); + } + } + + public void onApplicationEvent(@NonNull ContextClosedEvent event) { + if (channel != null) { + channel.closeFuture(); + boss.shutdownGracefully(); + worker.shutdownGracefully(); + } + log.info("TimiMusicRC player service shutdown finished"); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/bean/FileSyncConfig.java b/src/main/java/com/imyeyu/server/modules/system/bean/FileSyncConfig.java new file mode 100644 index 0000000..a855145 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/bean/FileSyncConfig.java @@ -0,0 +1,50 @@ +package com.imyeyu.server.modules.system.bean; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * 文件同步配置 + * + * @author 夜雨 + * @since 2024-12-09 14:14 + */ +@Data +public class FileSyncConfig { + + /** true 为启用同步功能 */ + private boolean enable; + + /** 轮询同步任务 cron 周期表达式,如果配置已启用但任务无效时自动创建 */ + private String cron; + + /** 同步任务列表 */ + private Map tasks; + + /** + * 同步任务 + * + * @author 夜雨 + * @since 2024-12-09 14:14 + */ + @Data + public static class Task { + + /** true 为启用此同步任务 */ + private boolean enable; + + /** true 为使用 MD5 校验,将会大幅增加扫描耗时 */ + private boolean useMD5Compare; + + /** 任务 cron 周期表达式 */ + private String cron; + + /** 同步来源文件路径 */ + private String source; + + /** 同步去向文件路径,不可位于 {@link #source} 内 */ + private List targets; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/bean/ServerFile.java b/src/main/java/com/imyeyu/server/modules/system/bean/ServerFile.java new file mode 100644 index 0000000..89786c5 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/bean/ServerFile.java @@ -0,0 +1,250 @@ +package com.imyeyu.server.modules.system.bean; + +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.TimiServerAPI; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.service.SettingService; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.FileImageInputStream; +import javax.imageio.stream.ImageInputStream; +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.util.Iterator; + +/** + * 文件属性 + * + * @author 夜雨 + * @version 2022-01-01 23:04 + */ +@Data +@Slf4j +public class ServerFile implements Serializable { + + /** + * 文件类型 + * + * @author 夜雨 + * @version 2022-07-05 14:45 + */ + @Getter + public enum FileType { + + DIRECTORY, + FILE, + TXT, + CODE, + SCRIPT, + ZIP, + MICROSOFT, + JAVA, + VIDEO, + FONT, + AUDIO, + IMAGE, + SYSTEM; + + String[] extensions; + + /** + * 字符串格式查找文件类型 + * + * @param extension 字符串格式 + * @return 文件类型 + */ + public static FileType fromExtension(String extension) { + FileType[] types = values(); + for (int i = 0; i < types.length; i++) { + final String[] extensions = types[i].getExtensions(); + for (int j = 0; j < extensions.length; j++) { + if (extensions[j].equalsIgnoreCase(extension)) { + return types[i]; + } + } + } + return FILE; + } + + public void setExtensions(String[] extensions) { + if (TimiJava.isNotEmpty(this.extensions)) { + throw new TimiException(TimiCode.ERROR, "readonly"); + } + this.extensions = extensions; + } + } + + /** + * 支持预览的扩展名 + * + * @author 夜雨 + * @version 2022-07-05 16:42 + */ + @Getter + @AllArgsConstructor + public enum SupportPreviewExtension { + + PNG("png"), + BMP("bmp"), + JPG("jpg"), + JPEG("jpeg"), + MP4("mp4"), + MOV("mov"); + + /** 名称 */ + final String name; + + /** + * 字符串格式查找文件类型 + * + * @param extension 字符串格式 + * @return 文件类型 + */ + public static SupportPreviewExtension fromExtension(String extension) { + SupportPreviewExtension[] extensions = values(); + for (int i = 0; i < extensions.length; i++) { + if (extensions[i].name.equalsIgnoreCase(extension)) { + return extensions[i]; + } + } + return null; + } + } + + // ---------- 基本属性 ---------- + + /** 文件名 */ + private String name; + + /** 扩展名 */ + private String extension; + + /** 绝对路径(非文件系统绝对路径,仅基于配置路径) */ + private String absolutePath; + + /** 大小 */ + private Long size; + + /** 修改时间 */ + private Long modifiedAt; + + /** 类型 */ + private FileType type; + + /** true 为文件 */ + private boolean isFile; + + /** true 为文件夹 */ + private boolean isDirectory; + + // ---------- 权限 ---------- + + /** true 为可读 */ + private boolean canRead; + + /** true 为可写 */ + private boolean canWrite; + + /** true 为隐藏 */ + private boolean isHidden; + + // ---------- 预览 ---------- + + /** true 为可预览 */ + private boolean canPreview; + + /** 最大预览宽度 */ + private int previewWidth; + + /** 最大预览高度 */ + private int previewHeight; + + /** 预览地址 */ + private String previewURI; + + public ServerFile(File file) { + name = file.getName(); + absolutePath = file.getAbsolutePath().replaceAll("\\\\", "/"); + SettingService settingService = TimiServerAPI.applicationContext.getBean(SettingService.class); + absolutePath = absolutePath.substring(settingService.getAsString(SettingKey.SYSTEM_FILE_BASE).length()); + + modifiedAt = file.lastModified(); + isFile = file.isFile(); + isDirectory = file.isDirectory(); + + canRead = file.canRead(); + canWrite = file.canWrite(); + isHidden = file.isHidden(); + previewWidth = previewHeight = -1; + + // 大小 + if (isFile) { + size = file.length(); + } + + // 类型 + type = isFile ? FileType.FILE : FileType.DIRECTORY; + if (isFile) { + if (name.lastIndexOf('.') != -1) { + extension = name.substring(name.lastIndexOf('.') + 1); + type = FileType.fromExtension(extension); + } + } + + // 预览 + SupportPreviewExtension previewExtension = SupportPreviewExtension.fromExtension(extension); + if (previewExtension != null) { + try { + // 解析预览最大尺寸 + switch (previewExtension) { + case PNG, BMP, JPG, JPEG -> { + // 图片 + Iterator readerIterator = ImageIO.getImageReadersBySuffix(extension); + if (readerIterator.hasNext()) { + ImageReader reader = readerIterator.next(); + try { + ImageInputStream stream = new FileImageInputStream(file); + reader.setInput(stream); + previewWidth = reader.getWidth(reader.getMinIndex()); + previewHeight = reader.getHeight(reader.getMinIndex()); + } catch (IOException e) { + log.info(e.getMessage(), e); + } finally { + reader.dispose(); + } + } else { + log.error("No reader found for given format: {}", extension); + } + } + } + canPreview = true; + previewURI = "/system/file/preview" + absolutePath; + } catch (Exception e) { + log.error("parse file preview image fail", e); + } + } + } + + /** @return true 为可读 */ + public boolean canRead() { + return canRead; + } + + /** @return true 为可写 */ + public boolean canWrite() { + return canWrite; + } + + /** @return true 为可预览 */ + public boolean canPreview() { + return false; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/bean/ServerStatus.java b/src/main/java/com/imyeyu/server/modules/system/bean/ServerStatus.java new file mode 100644 index 0000000..34f9e5d --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/bean/ServerStatus.java @@ -0,0 +1,246 @@ +package com.imyeyu.server.modules.system.bean; + +import lombok.Data; +import com.imyeyu.java.TimiJava; +import org.springframework.stereotype.Component; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; + +/** + * 服务器状态数据,所有动态数据左出右进,此对象由 IOC 托管 + * + * @author 夜雨 + * @version 2022-01-31 15:35 + */ +@Data +@Component +public class ServerStatus implements TimiJava { + + /** 动态数据更新时轴 */ + private LinkedList updateAxis = new LinkedList<>(); + + /** 系统 */ + private OS os = new OS(); + + /** CPU 使用率 */ + private CPU cpu = new CPU(); + + /** 系统内存 */ + private Memory memory = new Memory(); + + /** 网络 */ + private Network network = new Network(); + + /** 本程序状态 */ + private JVM jvm = new JVM(); + + /** 磁盘 */ + private List partitions = new ArrayList<>(); + + /** + * 系统 + * + * @author 夜雨 + * @version 2022-08-12 20:55 + */ + @Data + public static class OS { + + /** 名称 */ + private String name; + + /** 启动时间 */ + private long bootAt; + } + + /** + * 虚拟机状态 + * + * @author 夜雨 + * @version 2022-01-31 21:10 + */ + @Data + public static class JVM { + + /** 启动时间 */ + private long bootAt; + + /** JVM 名称 */ + private String name; + + /** JVM 版本 */ + private String version; + + /** 内存 */ + private Memory memory = new Memory(); + + /** 内存回收 */ + private ZGC zgc = new ZGC(); + + /** + * 内存 + * + * @author 夜雨 + * @version 2022-08-12 20:32 + */ + @Data + public static class Memory { + + /** 初始化 */ + private long init; + + /** 最大 */ + private long max; + + /** 已使用 */ + private final Deque used = new ArrayDeque<>(); + + /** 已提交 */ + private final Deque committed = new ArrayDeque<>(); + } + + /** + * 内存回收 + * + * @author 夜雨 + * @version 2022-08-12 20:32 + */ + @Data + public static class ZGC { + + /** 并发周期 */ + private long syncCycles = 0; + + /** 累计并发周期耗时(毫秒) */ + private long syncCyclesTimeTotal = 0; + + /** 累计次数 */ + private long pauses = 0; + + /** 累计回收暂停时长(毫秒) */ + private long pausesTimeTotal = 0; + + /** 上一次回收时间 */ + private long lastPauseAt = 0; + + /** 上一次回收大小 */ + private long lastRecoverySize = 0; + + /** 并发周期耗时 */ + private final Deque syncCyclesTime = new ArrayDeque<>(); + + /** 回收暂停时长 */ + private final Deque pausesTime = new ArrayDeque<>(); + } + } + + /** + * 中央处理器 + * + * @author 夜雨 + * @version 2022-01-31 15:40 + */ + @Data + public static class CPU { + + /** 名称 */ + private String name; + + /** 物理核心数量 */ + private int coreCount; + + /** 线程数量 */ + private int logicalCount; + + /** 温度 */ + private double temperature; + + /** 系统使用 */ + private final Deque system = new ArrayDeque<>(); + + /** 总共已使用 */ + private final Deque used = new ArrayDeque<>(); + } + + /** + * 系统内存 + * + * @author 夜雨 + * @version 2022-01-31 15:50 + */ + @Data + public static class Memory { + + /** 物理内存大小 */ + private long size; + + /** 交换区大小 */ + private long swapSize; + + /** 已使用 */ + private final Deque used = new ArrayDeque<>(); + + /** 交换区已使用 */ + private final Deque swapUsed = new ArrayDeque<>(); + } + + /** + * 网卡网速 + * + * @author 夜雨 + * @version 2022-08-10 21:41 + */ + @Data + public static class Network { + + /** 累计接收 */ + private long recvTotal; + + /** 累计发送 */ + private long sentTotal; + + /** 实时接收速度 */ + private long recvNow; + + /** 实时发送速度 */ + private long sentNow; + + /** MAC 地址 */ + private String mac; + + /** 发送 */ + private final Deque sent = new ArrayDeque<>(); + + /** 接收 */ + private final Deque recv = new ArrayDeque<>(); + } + + /** + * 分区 + * + * @author 夜雨 + * @version 2022-01-31 20:19 + */ + @Data + public static class Partition { + + /** 识别 UUID */ + private String uuid; + + /** 路径 */ + private String path; + + /** 文件系统类型 */ + private String type; + + /** 已使用 */ + private long used; + + /** 总大小 */ + private long total; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/bean/TerminalPipe.java b/src/main/java/com/imyeyu/server/modules/system/bean/TerminalPipe.java new file mode 100644 index 0000000..d6473ce --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/bean/TerminalPipe.java @@ -0,0 +1,208 @@ +package com.imyeyu.server.modules.system.bean; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.CallbackArg; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.utils.Time; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + +/** + * 指令管道 + * + * @author 夜雨 + * @version 2022-07-23 10:41 + */ +@Slf4j +public class TerminalPipe implements TimiJava { + + /** + * 状态 + * + * @author 夜雨 + * @version 2022-07-24 10:41 + */ + public enum Status { + + /** 等待指令 */ + WAIT, + + /** 正在执行指令 */ + RUNNING, + + /** 死亡 */ + DIED, + } + + /** 指令结束标记 */ + private static final String COMMAND_END_FLAG = "TIMI_LINUX_API_COMMAND_END"; + + /** 进程 */ + @Getter + private final Process process; + + /** 管道锁 */ + @Getter + private final Object lock; + + /** 管道状态 */ + @Getter + private Status status; + + /** 最后一次执行指令时间 */ + @Getter + private long lastExecAt; + + private final PrintWriter writer; + private final OutputStream os; + private final BufferedWriter bw; + private final OutputStreamWriter osw; + + private boolean canUnlock; + private CallbackArg callback; + + public TerminalPipe(Process process) { + this.lastExecAt = Time.now(); + this.process = process; + this.status = Status.WAIT; + this.lock = new Object(); + + // 指令输出 + os = process.getOutputStream(); + osw = new OutputStreamWriter(os); + bw = new BufferedWriter(osw); + writer = new PrintWriter(bw, true); + + // 默认管道 + SyncPipe def = new SyncPipe(process.getInputStream()) { + + @Override + public void onUpdate(String content) { + if (canUnlock) { + // 指令结束,返回当前路径 + callback.handler("[" + content.trim() + "]#\r\n"); + TerminalPipe.this.status = Status.WAIT; + // 放行管道锁 + synchronized (lock) { + lock.notifyAll(); + } + } else { + // 未结束,正常回调 + canUnlock = content.trim().equals(COMMAND_END_FLAG); + if (!canUnlock) { + callback.handler(content); + } + } + } + }; + // 错误管道 + SyncPipe error = new SyncPipe(process.getErrorStream()) { + + @Override + public void onUpdate(String content) { + callback.handler(content); + } + }; + + Thread defThread = new Thread(def); + Thread errThread = new Thread(error); + defThread.setName("def pipe"); + errThread.setName("err pipe"); + defThread.setDaemon(true); + errThread.setDaemon(true); + defThread.start(); + errThread.start(); + } + + /** + * 执行指令 + * + * @param callback 结果回调 + * @param command 指令 + */ + public void exec(CallbackArg callback, String command) { + if (status == Status.DIED) { + throw new TimiException(TimiCode.RESULT_BAD).msgKey("该会话已终止"); + } + this.status = Status.RUNNING; + this.callback = callback; + this.lastExecAt = Time.now(); + + canUnlock = false; + + writer.println(command); + writer.println("echo " + COMMAND_END_FLAG); + writer.println("pwd"); + } + + /** 销毁管道进程 */ + public void destroy() { + try { + writer.close(); + bw.close(); + osw.close(); + os.close(); + + process.destroyForcibly(); + + status = Status.DIED; + + synchronized (lock) { + lock.notifyAll(); + } + } catch (IOException e) { + log.error("destroy process fail", e); + } + } + + /** + * 指令执行异步管道 + * + * @author 夜雨 + * @version 2022-04-13 21:35 + */ + @Slf4j + private abstract static class SyncPipe implements Runnable { + + InputStream is; + + public SyncPipe(InputStream is) { + this.is = is; + } + + @Override + public void run() { + try { + InputStreamReader isr = new InputStreamReader(is); + BufferedReader in = new BufferedReader(isr); + String line; + while ((line = in.readLine()) != null) { + onUpdate(line + "\r\n"); + } + log.info("{} close stream", Thread.currentThread().getName()); + in.close(); + isr.close(); + is.close(); + } catch (IOException e) { + log.error(Thread.currentThread().getName() + " thread error", e); + } + } + + /** + * 响应内容 + * + * @param content 内容 + */ + public abstract void onUpdate(String content); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/bean/TransferFile.java b/src/main/java/com/imyeyu/server/modules/system/bean/TransferFile.java new file mode 100644 index 0000000..3982fec --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/bean/TransferFile.java @@ -0,0 +1,19 @@ +package com.imyeyu.server.modules.system.bean; + +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +/** + * 上传文件对象 + * + * @author 夜雨 + * @version 2022-01-13 00:51 + */ +@Data +public class TransferFile { + + private Long length; + private String name, path; + + private MultipartFile file; +} diff --git a/src/main/java/com/imyeyu/server/modules/system/controller/AsyncTaskController.java b/src/main/java/com/imyeyu/server/modules/system/controller/AsyncTaskController.java new file mode 100644 index 0000000..d2d2c89 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/controller/AsyncTaskController.java @@ -0,0 +1,70 @@ +package com.imyeyu.server.modules.system.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.server.modules.system.service.AsyncTaskService; +import com.imyeyu.server.modules.system.service.implement.AbstractAsyncTask; +import com.imyeyu.server.modules.system.task.async.DebugAsyncTask; +import com.imyeyu.server.modules.system.vo.AsyncTaskView; +import com.imyeyu.spring.annotation.AOPLog; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 异步任务接口 + * + * @author 夜雨 + * @version 2022-08-13 19:38 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/system/task") +public class AsyncTaskController { + + private final AsyncTaskService service; + + @AOPLog + @PostMapping("/debug") + public String debug(@RequestParam("second") Integer second) { + if (second == null) { + second = 60; + } + DebugAsyncTask task = new DebugAsyncTask(second); + service.addAsyncTask(task); + return task.getUuid(); + } + + @GetMapping("/detail") + public AbstractAsyncTask detail(@RequestParam ("uuid") String uuid) { + return service.getAsyncTask(uuid); + } + + @PostMapping("/list") + public List list() { + return service.listAllView(); + } + + @AOPLog + @PostMapping("/start") + public void start(@RequestParam ("uuid") String uuid) { + service.start(uuid); + } + + @AOPLog + @PostMapping("/pause") + public void pause(@RequestParam ("uuid") String uuid) { + service.pause(uuid); + } + + @AOPLog + @PostMapping("/interrupt") + public void interrupt(@RequestParam ("uuid") String uuid) { + service.interrupt(uuid); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/controller/FileController.java b/src/main/java/com/imyeyu/server/modules/system/controller/FileController.java new file mode 100644 index 0000000..0a3ebbd --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/controller/FileController.java @@ -0,0 +1,493 @@ +package com.imyeyu.server.modules.system.controller; + +import com.imyeyu.io.IO; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.network.Network; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.entity.Tag; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.server.modules.common.service.TagService; +import com.imyeyu.server.modules.common.vo.tag.TagRequest; +import com.imyeyu.server.modules.system.bean.ServerFile; +import com.imyeyu.server.modules.system.bean.TransferFile; +import com.imyeyu.server.modules.system.service.AsyncTaskService; +import com.imyeyu.server.modules.system.service.FileService; +import com.imyeyu.server.modules.system.task.async.FileCalcSizeAsyncTask; +import com.imyeyu.server.modules.system.task.async.FileCopyAsyncTask; +import com.imyeyu.server.modules.system.task.async.FileMoveAsyncTask; +import com.imyeyu.server.modules.system.task.async.FileTarAsyncTask; +import com.imyeyu.server.modules.system.task.async.FileUnTarAsyncTask; +import com.imyeyu.server.modules.system.task.async.FileUnZipAsyncTask; +import com.imyeyu.server.modules.system.task.async.FileZipAsyncTask; +import com.imyeyu.server.modules.system.util.ResourceHandler; +import com.imyeyu.server.modules.system.vo.ListFileToRequest; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.IgnoreGlobalReturn; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.utils.OS; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.coobird.thumbnailator.Thumbnails; +import org.apache.tika.Tika; +import org.jcodec.api.FrameGrab; +import org.jcodec.common.model.Picture; +import org.jcodec.scale.AWTUtil; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; + +/** + * 文件系统接口 + * + * @author 夜雨 + * @version 2022-01-01 20:00 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/system/file") +public class FileController implements TimiJava, OS.FileSystem { + + private final TagService tagService; + private final FileService service; + private final SettingService settingService; + private final AsyncTaskService asyncTaskService; + + private final ResourceHandler resourceHandler; + + /** + * 文件对象 + * + * @param req 请求 + * @param resp 返回 + * @return 元数据对象 + */ + @AOPLog + @RequestRateLimit + @GetMapping("/object/**") + public ServerFile object(HttpServletRequest req, HttpServletResponse resp) { + String path = req.getServletPath().substring("/system/file/object".length()); + if (TimiJava.isEmpty(path)) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("TODO 缺少参数:object"); + } + ServerFile file = new ServerFile(new File(path)); + service.checkAccessPermission(file.getAbsolutePath()); + return file; + } + + @AOPLog + @RequestRateLimit + @PostMapping("/tag") + public void tag(@RequestBody TagRequest request) { + Tag tag = new Tag(); + tag.setBizType(Tag.BizType.SERVER_FILE); + tag.setBizID(request.getBizId()); + tag.setValue(request.getZhCN()); + tagService.create(tag); + } + + @AOPLog + @RequestRateLimit + @GetMapping("/tag/list") + public List listTag(@RequestParam String bizId) { + return tagService.listByBizID(Tag.BizType.SERVER_FILE, bizId); + } + + /** + * 深度搜索 + * + * @param params path: 基本路径, keyword: 关键字 + * @return 文件列表 + */ + @AOPLog + @RequestRateLimit + @PostMapping("/search") + public List search(@RequestBody Map params) { + if (TimiJava.isEmpty(params.get("path"))) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:path"); + } + if (TimiJava.isEmpty(params.get("keyword"))) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:keyword"); + } + return service.doFilter(service.search(params.get("path"), params.get("keyword"))); + } + + /** + * 文件是否存在 + * + * @param req 请求 + * @param resp 返回 + * @return true 为存在 + */ + @AOPLog + @RequestRateLimit + @GetMapping("/exist/**") + public boolean exist(HttpServletRequest req, HttpServletResponse resp) { + String path = req.getServletPath().substring("/system/file/exist".length()); + if (TimiJava.isEmpty(path)) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:path"); + } + return service.canAccess(path) && service.isExist(path); + } + + /** + * 文件夹大小 + * + * @param req 请求 + * @param resp 返回 + * @return 异步任务 UUID + */ + @AOPLog + @RequestRateLimit + @GetMapping("/size/**") + public String size(HttpServletRequest req, HttpServletResponse resp) { + String path = req.getServletPath().substring("/system/file/size".length()); + if (TimiJava.isEmpty(path)) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:path"); + } + File file = new File(path); + service.checkAccessPermission(file.getAbsolutePath()); + // 异步任务 + FileCalcSizeAsyncTask task = new FileCalcSizeAsyncTask(file); + asyncTaskService.addAsyncTask(task); + return task.getUuid(); + } + + /** + * 通用资源加载接口 + * + * @param req 请求 + * @param resp 返回 + */ + @AOPLog + @IgnoreGlobalReturn + @GetMapping("/read/**") + public void read(HttpServletRequest req, HttpServletResponse resp) { + try { + String path = req.getServletPath().substring("/system/file/read".length()); + path = settingService.getAsString(SettingKey.SYSTEM_FILE_BASE) + path; + if (TimiJava.isEmpty(path)) { + resp.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + service.checkAccessPermission(path); + Path filePath = Paths.get(path); + if (!Files.exists(filePath)) { + log.warn("read a not exist file: {}", path); + resp.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + String mimeType = new Tika().detect(filePath); + if (TimiJava.isNotEmpty(mimeType)) { + resp.setContentType(mimeType); + } + req.setAttribute(ResourceHandler.ATTR_TYPE, ResourceHandler.Type.FILE); + req.setAttribute(ResourceHandler.ATTR_VALUE, filePath); + resourceHandler.handleRequest(req, resp); + } catch (Exception e) { + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + /** + * 文件列表 + * + * @return 列表 + */ + @GetMapping("/list/**") + public List list(HttpServletRequest req) { + return service.doFilter(service.list(req.getServletPath().substring("/system/file/list".length()))); + } + + /** + * 预览接口 + * + * @param req 请求 + * @param resp 返回 + */ + @AOPLog + @IgnoreGlobalReturn + @GetMapping("/preview/**") + public void preview(HttpServletRequest req, HttpServletResponse resp) { + resp.setCharacterEncoding(StandardCharsets.UTF_8.toString()); + try { + String path = req.getServletPath().substring("/system/file/preview".length()); + if (TimiJava.isEmpty(path)) { + resp.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + path = settingService.getAsString(SettingKey.SYSTEM_FILE_BASE) + path; + File file = new File(path); + if (!file.exists()) { + log.warn("preview a not exist file: {}", path); + resp.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + service.checkAccessPermission(file.getAbsolutePath()); + String fileName = file.getName(); + if (fileName.lastIndexOf('.') == -1) { + log.warn("preview a not support file: {}", path); + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return; + } + String extension = fileName.substring(fileName.lastIndexOf('.') + 1); + ServerFile.SupportPreviewExtension ext = ServerFile.SupportPreviewExtension.fromExtension(extension); + if (ext == null) { + log.warn("preview a not support file: {}", path); + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return; + } + switch (ext) { + case PNG, BMP, JPG, JPEG -> { + // 图片 + String mimeType = new Tika().detect(file); + if (TimiJava.isNotEmpty(mimeType)) { + resp.setContentType(mimeType); + } + Thumbnails.of(file).size(256, 256).toOutputStream(resp.getOutputStream()); + } + case MP4, MOV -> { + // 视频 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Picture picture = FrameGrab.getFrameAtSec(file, 2); + BufferedImage bi = AWTUtil.toBufferedImage(picture); + ImageIO.write(bi, "png", baos); + Thumbnails.of(IO.toInputStream(baos)).size(256, 256).toOutputStream(resp.getOutputStream()); + } + } + } catch (Exception e) { + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + /** + * 创建文件夹 + * + * @param params path: 路径, name: 名称 + * @return true 为成功 + */ + @AOPLog + @RequestRateLimit + @PostMapping("/mkdir") + public String mkdir(@RequestBody Map params) { + if (TimiJava.isEmpty(params.get("path"))) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:path"); + } + if (TimiJava.isEmpty(params.get("name"))) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:name"); + } + return service.mkdir(params.get("path"), params.get("name")).getAbsolutePath(); + } + + /** + * 重命名文件 + * + * @param params from: 原文件, to: 目标文件 + * @return true 为成功 + */ + @AOPLog + @RequestRateLimit + @PostMapping("/rename") + public boolean rename(@RequestBody Map params) { + if (TimiJava.isEmpty(params.get("from"))) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:from"); + } + if (TimiJava.isEmpty(params.get("to"))) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:to"); + } + return service.rename(params.get("from"), params.get("to")); + } + + /** + * 压缩 ZIP + * + * @param listFileToRequest 文件列表操作对象 + * @return 异步任务 UUID + */ + @AOPLog + @RequestRateLimit + @PostMapping("/zip") + public String zip(@RequestBody ListFileToRequest listFileToRequest) { + if (TimiJava.isEmpty(listFileToRequest.getList())) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:list[]"); + } + if (TimiJava.isEmpty(listFileToRequest.getTo())) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:to"); + } + // 异步任务 + FileZipAsyncTask task = new FileZipAsyncTask(listFileToRequest.getList(), listFileToRequest.getTo()); + asyncTaskService.addAsyncTask(task); + return task.getUuid(); + } + + /** + * 解压缩 ZIP + * + * @param params zipFile: 压缩文件, to: 解压到 + * @return 异步任务 UUID + */ + @AOPLog + @RequestRateLimit + @PostMapping("/unzip") + public String unzip(@RequestBody Map params) { + String zipFilePath = params.get("zipFile"); + if (TimiJava.isEmpty(zipFilePath)) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:zipFile"); + } + File zipFile = new File(zipFilePath); + if (!zipFile.exists()) { + throw new TimiException(TimiCode.ARG_BAD).msgKey("文件不存在:" + zipFilePath); + } + if (TimiJava.isEmpty(params.get("to"))) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:to"); + } + // 异步任务 + FileUnZipAsyncTask task = new FileUnZipAsyncTask(zipFile, params.get("to")); + asyncTaskService.addAsyncTask(task); + return task.getUuid(); + } + + /** + * 压缩 TAR + * + * @param listFileToRequest 文件列表操作对象 + * @return 异步任务 UUID + */ + @AOPLog + @RequestRateLimit + @PostMapping("/tar") + public String tar(@RequestBody ListFileToRequest listFileToRequest) { + FileTarAsyncTask task = new FileTarAsyncTask(listFileToRequest.getList(), listFileToRequest.getTo()); + asyncTaskService.addAsyncTask(task); + return task.getUuid(); + } + + /** + * 解压缩 TAR + * + * @param params tarFile: 压缩文件, to: 解压到 + * @return 异步任务 UUID + */ + @AOPLog + @RequestRateLimit + @PostMapping("/untar") + public String untar(@RequestBody Map params) { + // TODO 具名入参 + String tarFilePath = params.get("tarFile"); + if (TimiJava.isEmpty(tarFilePath)) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:tarFile"); + } + File tarFile = new File(tarFilePath); + if (!tarFile.exists()) { + throw new TimiException(TimiCode.ARG_BAD).msgKey("文件不存在:" + tarFilePath); + } + if (TimiJava.isEmpty(params.get("to"))) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:to"); + } + FileUnTarAsyncTask task = new FileUnTarAsyncTask(tarFile, params.get("to")); + asyncTaskService.addAsyncTask(task); + return task.getUuid(); + } + + /** + * 剪切 + * + * @param listFileToRequest 文件列表操作对象 + * @return 异步任务 UUID + */ + @AOPLog + @RequestRateLimit + @PostMapping("/cut") + public String cut(@RequestBody ListFileToRequest listFileToRequest) { + FileMoveAsyncTask task = new FileMoveAsyncTask(listFileToRequest.getList(), listFileToRequest.getTo()); + asyncTaskService.addAsyncTask(task); + return task.getUuid(); + } + + /** + * 复制 + * + * @param listFileToRequest 文件列表操作对象 + * @return 异步任务 UUID + */ + @AOPLog + @RequestRateLimit + @PostMapping("/copy") + public String copy(@RequestBody ListFileToRequest listFileToRequest) { + FileCopyAsyncTask task = new FileCopyAsyncTask(listFileToRequest.getList(), listFileToRequest.getTo()); + asyncTaskService.addAsyncTask(task); + return task.getUuid(); + } + + /** + * 上传文件 + * + * @param transferFile 文件传输对象 + */ + @AOPLog + @RequestRateLimit + @PostMapping("/upload") + public void upload(TransferFile transferFile) { + service.upload(transferFile); + } + + /** + * 下载文件 + * + * @param req 请求对象 + * @param resp 回调对象 + */ + @AOPLog + @RequestRateLimit + @IgnoreGlobalReturn + @RequestMapping("/download/**") + public void download(HttpServletRequest req, HttpServletResponse resp) { + try { + String path = req.getServletPath().substring("/system/file/download".length()); + path = settingService.getAsString(SettingKey.SYSTEM_FILE_BASE) + path; + File file = new File(path); + service.checkAccessPermission(file.getAbsolutePath()); + String mimeType = new Tika().detect(file); + resp.setContentType(mimeType); + resp.setHeader("Content-Disposition", Network.getFileDownloadHeader(file.getName())); + resp.setHeader("Content-Range", String.valueOf(file.length() - 1)); + resp.setHeader("Accept-Ranges", "bytes"); + resp.setContentLengthLong(file.length()); + IO.toOutputStream(resp.getOutputStream(), file); + resp.flushBuffer(); + } catch (Exception e) { + log.error("download error", e); + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + /** + * 销毁文件 + * + * @param list 服务器文件绝对路径列表 + */ + @AOPLog + @RequestRateLimit + @PostMapping("/destroy") + public void destroy(@RequestBody String[] list) { + service.destroy(list); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/controller/SystemController.java b/src/main/java/com/imyeyu/server/modules/system/controller/SystemController.java new file mode 100644 index 0000000..a015e71 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/controller/SystemController.java @@ -0,0 +1,135 @@ +package com.imyeyu.server.modules.system.controller; + +import com.imyeyu.io.IO; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.common.service.AttachmentService; +import com.imyeyu.server.modules.common.vo.attachment.AttachmentRequest; +import com.imyeyu.server.modules.system.bean.ServerStatus; +import com.imyeyu.server.modules.system.service.SystemService; +import com.imyeyu.server.modules.system.vo.TempAttachRequest; +import com.imyeyu.spring.annotation.AOPLog; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.concurrent.Semaphore; + +/** + * 服务器控制接口 + * + * @author 夜雨 + * @version 2022-01-31 22:47 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/system/server") +public class SystemController { + + private final ServerStatus serverStatus; + private final SystemService service; + private final AttachmentService attachmentService; + + private final Semaphore updateSemaphore = new Semaphore(1); + private final Semaphore rebootSemaphore = new Semaphore(1); + private final Semaphore restoreSemaphore = new Semaphore(1); + + /** @return 实时服务器状态 */ + @RequestMapping("/status") + public ServerStatus getStatus() { + return serverStatus; + } + + /** + * 更新系统 + * + * @param file + */ + @AOPLog + @PostMapping("/update") + public void update(@NotNull @RequestParam("file") MultipartFile file) { + if (updateSemaphore.tryAcquire()) { + try { + service.update(file); + } finally { + updateSemaphore.release(); + } + } else { + throw new TimiException(TimiCode.ERROR_SERVICE_OFF).msgKey("TODO updating"); + } + } + + /** + * 恢复系统 + */ + @AOPLog + @RequestMapping("/restore") + public void restore() { + if (restoreSemaphore.tryAcquire()) { + try { + service.restore(); + } finally { + restoreSemaphore.release(); + } + } else { + throw new TimiException(TimiCode.ERROR_SERVICE_OFF).msgKey("TODO updating"); + } + } + + /** + * 停止系统 + * + */ + @AOPLog + @RequestMapping("/shutdown") + public void shutdown() { + service.shutdown(); + } + + /** + * 重启系统 + */ + @AOPLog + @RequestMapping("/reboot") + public void reboot() { + if (rebootSemaphore.tryAcquire()) { + try { + service.reboot(); + } finally { + rebootSemaphore.release(); + } + } else { + throw new TimiException(TimiCode.ERROR_SERVICE_OFF).msgKey("TODO rebooting"); + } + } + + // TODO 临时接口 + @AOPLog + @PostMapping("/attach") + public void uploadAttachment(TempAttachRequest request) { + try { + for (MultipartFile file : request.getFile()) { + byte[] bytes = IO.toBytes(file.getInputStream()); + + AttachmentRequest attach = new AttachmentRequest(); + attach.setName(file.getOriginalFilename()); + attach.setBizType(request.getBizType()); + attach.setBizId(request.getBizId()); + attach.setAttachType(request.getAttachType()); + attach.setSize((long) bytes.length); + attach.setInputStream(new ByteArrayInputStream(bytes)); + attachmentService.create(attach); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/controller/TerminalController.java b/src/main/java/com/imyeyu/server/modules/system/controller/TerminalController.java new file mode 100644 index 0000000..c67a902 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/controller/TerminalController.java @@ -0,0 +1,100 @@ +package com.imyeyu.server.modules.system.controller; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.server.modules.system.service.TerminalService; +import com.imyeyu.server.modules.system.vo.terminal.ExecCommand; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestSingleParam; +import com.imyeyu.spring.annotation.RequiredToken; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * 控制台系统 + * + * @author 夜雨 + * @version 2022-07-22 10:04 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/system/terminal") +public class TerminalController { + + private final SettingService settingService; + private final TerminalService service; + + /** + * 创建会话 + * + * @return 会话 ID + */ + @AOPLog + @PostMapping("/create") + public String create() { + return service.create(); + } + + /** + * 会话是否存活 + * + * @param sessionId 会话 ID + * @return true 为存活状态 + */ + @RequiredToken + @PostMapping("/alive") + public boolean isAlive(@RequestSingleParam String sessionId) { + return service.isAlive(sessionId); + } + + /** + * 指令路径补充 + * + * @param path 路径 + * @return 补充的字符 + */ + @AOPLog + @RequiredToken + @PostMapping("/fill") + public synchronized String pathFill(@RequestSingleParam String path) { + return service.pathFill(path); + } + + /** + * 执行指令 + * + * @param resp 服务回调 + */ + @RequiredToken + @PostMapping("/exec") + public synchronized void exec(@RequestBody ExecCommand execCommand, HttpServletResponse resp) { + service.exec(execCommand, s -> { + try { + resp.getOutputStream().write(s.getBytes(StandardCharsets.UTF_8)); + resp.getOutputStream().flush(); + } catch (IOException e) { + log.error("command callback response error", e); + } + }); + } + + /** + * 关闭会话 + * + * @param sessionId 会话 ID + */ + @AOPLog + @RequiredToken + @PostMapping("/close") + public void close(@RequestSingleParam String sessionId) { + service.close(sessionId); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/entity/AsyncTask.java b/src/main/java/com/imyeyu/server/modules/system/entity/AsyncTask.java new file mode 100644 index 0000000..82949a7 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/entity/AsyncTask.java @@ -0,0 +1,134 @@ +package com.imyeyu.server.modules.system.entity; + +import com.imyeyu.spring.annotation.Entity; +import lombok.Data; + +/** + * 异步任务 + * + * @author 夜雨 + * @version 2022-08-14 08:12 + */ +@Data +@Entity +public class AsyncTask { + + /** + * 状态 + * + * @author 夜雨 + * @version 创建于 2022-08-13 13:05 + */ + public enum Status { + + /** 空闲 */ + IDLE, + + /** 等待状态(周期性任务状态) */ + WAITING, + + /** 进行中 */ + RUNNING, + + /** 暂停 */ + PAUSE, + + /** 中断 */ + INTERRUPT, + + /** 死亡 */ + DIED, + + /** 错误 */ + ERROR + } + + /** + * 任务类型 + * + * @author 夜雨 + * @version 2022-08-15 16:01 + */ + public enum Type { + + /** 调试 */ + DEBUG, + + /** 文件同步 */ + FILE_SYNC, + + /** 移动文件 */ + FILE_MOVE, + + /** 移动复制 */ + FILE_COPY, + + /** 文件压缩 ZIP */ + FILE_ZIP, + + /** 文件解压缩 ZIP */ + FILE_UNZIP, + + /** 文件压缩 TAR */ + FILE_TAR, + + /** 文件解压缩 TAR */ + FILE_UNTAR, + + /** 计算文件夹大小 */ + FILE_CALC_SIZE + } + + /** 任务 ID */ + protected String uuid; + + /** 名称 */ + protected String name; + + /** 任务类型 */ + protected Type type; + + /** 提示消息 */ + private String message; + + /** 当前状态 */ + protected Status status; + + /** 进度 */ + protected double progress; + + /** true 为允许暂停 */ + protected boolean canPause; + + /** true 为可中断 */ + protected boolean canInterrupt; + + /** true 为周期性任务 */ + protected boolean isPeriodical; + + /** {@link #isPeriodical} 为 true 时的周期时长 */ + protected String cron; + + /** 开始时间 */ + protected long startAt; + + /** 开始时间 */ + protected long interruptAt; + + /** 失败时间 */ + protected long errorAt; + + /** 死亡时间 */ + protected long diedAt; + + /** 创建时间 */ + protected long createdAt; + + public boolean canPause() { + return canPause; + } + + public boolean canInterrupt() { + return canInterrupt; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/mapper/AsyncTaskMapper.java b/src/main/java/com/imyeyu/server/modules/system/mapper/AsyncTaskMapper.java new file mode 100644 index 0000000..a27c7a6 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/mapper/AsyncTaskMapper.java @@ -0,0 +1,18 @@ +package com.imyeyu.server.modules.system.mapper; + +import com.imyeyu.server.modules.system.entity.AsyncTask; +import org.apache.ibatis.annotations.Select; + +/** + * @author 夜雨 + * @since 2024-12-20 16:35 + */ +public interface AsyncTaskMapper { + + void insert(AsyncTask asyncTask); + + @Select("SELECT * FROM async_task WHERE uuid = #{uuid}") + AsyncTask select(String uuid); + + void update(AsyncTask asyncTask); +} diff --git a/src/main/java/com/imyeyu/server/modules/system/service/AsyncTaskService.java b/src/main/java/com/imyeyu/server/modules/system/service/AsyncTaskService.java new file mode 100644 index 0000000..2e03847 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/service/AsyncTaskService.java @@ -0,0 +1,30 @@ +package com.imyeyu.server.modules.system.service; + +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.system.service.implement.AbstractAsyncTask; +import com.imyeyu.server.modules.system.vo.AsyncTaskView; + +import java.util.List; + +/** + * @author 夜雨 + * @since 2024-12-20 17:39 + */ +public interface AsyncTaskService { + + void addAsyncTask(AbstractAsyncTask task) throws TimiException; + + AbstractAsyncTask getAsyncTask(String uuid); + + void start(String uuid) throws TimiException; + + void pause(String uuid) throws TimiException; + + void interrupt(String uuid) throws TimiException; + + void removeAsyncTask(AbstractAsyncTask task) throws TimiException; + + List listAll(); + + List listAllView(); +} diff --git a/src/main/java/com/imyeyu/server/modules/system/service/FileService.java b/src/main/java/com/imyeyu/server/modules/system/service/FileService.java new file mode 100644 index 0000000..43ed6ed --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/service/FileService.java @@ -0,0 +1,114 @@ +package com.imyeyu.server.modules.system.service; + +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.system.bean.ServerFile; +import com.imyeyu.server.modules.system.bean.TransferFile; + +import java.io.File; +import java.io.InputStream; +import java.util.List; + +/** + * 文件服务 + * + * @author 夜雨 + * @version 2022-01-07 16:03 + */ +public interface FileService { + + boolean canAccess(String absPath) throws TimiException; + + default boolean canAccess(File file) throws TimiException { + return canAccess(file.getAbsolutePath()); + } + + void checkAccessPermission(String absPath) throws TimiException; + + default void checkAccessPermission(File file) throws TimiException { + checkAccessPermission(file.getAbsolutePath()); + } + + List doFilter(List list) throws TimiException; + + /** + * 文件列表 + * + * @param path 路径 + * @return 文件列表 + * @throws TimiException 服务异常 + */ + List list(String path); + + /** + * 文件是否存在 + * + * @param path 路径 + * @return true 为存在 + * @throws TimiException 服务异常 + */ + boolean isExist(String path); + + /** + * 深度搜索 + * + * @param path 基本路径 + * @param keyword 关键字 + * @return 文件列表 + * @throws TimiException 服务异常 + */ + List search(String path, String keyword); + + /** + * 创建文件夹 + * + * @param path 路径 + * @param name 名称 + * @return 文件夹 + * @throws TimiException 服务异常 + */ + File mkdir(String path, String name); + + /** + * 重命名,如果原文件不存在则修改失败 + * + * @param from 原文件 + * @param to 目标文件 + * @return true 为成功 + * @throws TimiException 服务异常 + */ + boolean rename(String from, String to); + + /** + * 获取文件数据流 + * + * @param path 绝对路径 + * @return 数据流 + * @throws TimiException 服务异常 + */ + InputStream getInputStream(String path); + + /** + * 上传文件 + * + * @param file 文件传输对象 + * @throws TimiException 服务异常 + */ + void upload(TransferFile file); + + /** + * 数据流写入到文件 + * + * @param file 文件 + * @param is 数据流 + * @throws TimiException 服务异常 + */ + void toFile(File file, InputStream is); + + /** + * 删除文件 + * + * @param list 列表 + * @throws TimiException 服务异常 + */ + void destroy(String... list); +} diff --git a/src/main/java/com/imyeyu/server/modules/system/service/SystemService.java b/src/main/java/com/imyeyu/server/modules/system/service/SystemService.java new file mode 100644 index 0000000..a29603d --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/service/SystemService.java @@ -0,0 +1,18 @@ +package com.imyeyu.server.modules.system.service; + +import org.springframework.web.multipart.MultipartFile; + +/** + * @author 夜雨 + * @version 2024-03-13 00:44 + */ +public interface SystemService { + + void update(MultipartFile file); + + void restore(); + + void shutdown(); + + void reboot(); +} diff --git a/src/main/java/com/imyeyu/server/modules/system/service/TerminalService.java b/src/main/java/com/imyeyu/server/modules/system/service/TerminalService.java new file mode 100644 index 0000000..a322c81 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/service/TerminalService.java @@ -0,0 +1,63 @@ +package com.imyeyu.server.modules.system.service; + +import com.imyeyu.java.bean.CallbackArg; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.system.bean.TerminalPipe; +import com.imyeyu.server.modules.system.vo.terminal.ExecCommand; + +import java.util.Map; + +/** + * 指令服务 + * + * @author 夜雨 + * @version 2022-07-22 10:27 + */ +public interface TerminalService { + + /** @return 当前会话列表 */ + Map listTerminalPipe(); + + /** + * 创建会话 + * + * @return 会话 ID + * @throws TimiException 服务异常 + */ + String create(); + + /** + * 会话是否存活 + * + * @param sessionID 会话 ID + * @return true 为存活状态 + * @throws TimiException 服务异常 + */ + boolean isAlive(String sessionID); + + /** + * 路径快速补全 + * + * @param path 路径 + * @return 需补全的字符 + * @throws TimiException 服务异常 + */ + String pathFill(String path); + + /** + * 执行指令(执行锁为 Session 对象,即每个会话应当同步 Session 锁,防止执行冲突) + * + * + * @param callback 结果回调 + * @throws TimiException 服务异常 + */ + void exec(ExecCommand execCommand, CallbackArg callback); + + /** + * 关闭会话 + * + * @param sessionID 会话 ID + * @throws TimiException 服务异常 + */ + void close(String sessionID); +} diff --git a/src/main/java/com/imyeyu/server/modules/system/service/implement/AbstractAsyncTask.java b/src/main/java/com/imyeyu/server/modules/system/service/implement/AbstractAsyncTask.java new file mode 100644 index 0000000..d9b54f5 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/service/implement/AbstractAsyncTask.java @@ -0,0 +1,214 @@ +package com.imyeyu.server.modules.system.service.implement; + +import ch.qos.logback.classic.Level; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.TimiServerAPI; +import com.imyeyu.server.modules.system.entity.AsyncTask; +import com.imyeyu.utils.Time; +import org.springframework.scheduling.config.CronTask; +import org.springframework.scheduling.config.ScheduledTask; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.scheduling.support.CronTrigger; +import org.springframework.scheduling.support.SimpleTriggerContext; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.util.UUID; + +/** + * 抽象异步任务 + * + * @author 夜雨 + * @version 2022-08-13 10:40 + */ +@Slf4j +public abstract class AbstractAsyncTask extends AsyncTask { + + /** 暂停锁 */ + private final Object pauseLocker = new Object(); + + /** {@link #isPeriodical} 为 true 时的周期任务对象 */ + ScheduledTask scheduledTask; + + /** {@link #isPeriodical} 为 true 时的下一次执行时间戳 */ + long nextExecuteAt; + + /** 日志缓冲 */ + final StringBuilder logBuffer = new StringBuilder(); + + public AbstractAsyncTask() { + uuid = UUID.randomUUID().toString(); + status = AsyncTask.Status.IDLE; + progress = -1; + canPause = true; + canInterrupt = true; + } + + /** 启动 */ + final void startup() { + log(Level.INFO, "startup async task"); + startAt = Time.now(); + start(); + if (isPeriodical) { + // 周期性任务 + status = Status.WAITING; + + ScheduledTaskRegistrar registrar = TimiServerAPI.applicationContext.getBean(ScheduledTaskRegistrar.class); + scheduledTask = registrar.scheduleCronTask(new CronTask(() -> { + // TODO CORN 循环任务分批记录日志 + try { + try { + if (scheduledTask.getTask() instanceof CronTask cronTask && cronTask.getTrigger() instanceof CronTrigger cronTrigger) { + nextExecuteAt = cronTrigger.nextExecution(new SimpleTriggerContext()).toEpochMilli(); + } + AbstractAsyncTask.this.run(); + } catch (TimiException e) { + if (e.getCode() == TimiCode.ERROR_SERVICE_OFF) { + interrupt(); + } else { + throw e; + } + } + } catch (Exception e) { + status = Status.ERROR; + errorAt = Time.now(); + log(e); + } finally { + progress = -1; + if (!isInterrupt()) { + status = Status.WAITING; + if (scheduledTask.getTask() instanceof CronTask cronTask && cronTask.getTrigger() instanceof CronTrigger cronTrigger) { + nextExecuteAt = cronTrigger.nextExecution(new SimpleTriggerContext()).toEpochMilli(); + } + log(Level.INFO, "waiting period task, next execute at %s".formatted(Time.toDateTime(nextExecuteAt))); + } + } + }, cron)); + if (scheduledTask.getTask() instanceof CronTask cronTask && cronTask.getTrigger() instanceof CronTrigger cronTrigger) { + nextExecuteAt = cronTrigger.nextExecution(new SimpleTriggerContext()).toEpochMilli(); + log(Level.INFO, "waiting period task, next execute at %s".formatted(Time.toDateTime(nextExecuteAt))); + } + } else { + // 一次性任务 + status = Status.RUNNING; + Thread thread = new Thread(() -> { + try { + try { + run(); + } catch (TimiException e) { + if (e.getCode() == TimiCode.ERROR_SERVICE_OFF) { + interrupt(); + } + } + } catch (Exception e) { + status = Status.ERROR; + errorAt = Time.now(); + log(e); + } + }); + thread.setDaemon(true); + thread.start(); + } + } + + /** 开始 */ + void start() { + if (status != AsyncTask.Status.IDLE && status != AsyncTask.Status.PAUSE) { + throw new TimiException(TimiCode.RESULT_BAD).msgKey("can not start this task"); + } + synchronized (pauseLocker) { + pauseLocker.notifyAll(); + } + onStart(); + } + + /** 暂停 */ + void pause() { + if (!canPause || (status != AsyncTask.Status.WAITING && status != AsyncTask.Status.RUNNING)) { + throw new TimiException(TimiCode.RESULT_BAD).msgKey("can not pause this task"); + } + status = Status.PAUSE; + onPause(); + } + + /** 中断 */ + void interrupt() { + if (isInterrupt()) { + return; + } + if (!canInterrupt || (status != AsyncTask.Status.WAITING && status != AsyncTask.Status.RUNNING && status != AsyncTask.Status.PAUSE)) { + throw new TimiException(TimiCode.RESULT_BAD).msgKey("can not interrupt this task"); + } + if (status == AsyncTask.Status.PAUSE) { + status = AsyncTask.Status.INTERRUPT; + synchronized (pauseLocker) { + pauseLocker.notifyAll(); + } + } + if (isPeriodical && scheduledTask != null) { + scheduledTask.cancel(false); + scheduledTask = null; + } + status = AsyncTask.Status.INTERRUPT; + interruptAt = Time.now(); + onInterrupt(); + } + + /** 执行 */ + protected abstract void run() throws Exception; + + /** @return true 为已暂停 */ + protected boolean isPause() { + return status == AsyncTask.Status.PAUSE; + } + + /** @return true 为已中止 */ + protected boolean isInterrupt() { + return status == AsyncTask.Status.INTERRUPT || status == AsyncTask.Status.DIED; + } + + /** 在耗时任务中执行此方法可受 {@link #isPause()} 和 {@link #isInterrupt()} 控制暂停或中断 */ + protected void pauseInterruptHandle() throws InterruptedException { + Thread.sleep(700); + if (isPause()) { + // 暂停 + synchronized (pauseLocker) { + pauseLocker.wait(); + } + status = AsyncTask.Status.RUNNING; + } + if (isInterrupt()) { + // 停止 + throw TimiCode.ERROR_SERVICE_OFF.toException(); + } + } + + /** 开始 */ + protected void onStart() { + // 子类实现 + } + + /** 暂停 */ + protected void onPause() { + // 子类实现 + } + + /** 停止 */ + protected void onInterrupt() { + // 子类实现 + } + + protected void log(Level level, String log) { + AbstractAsyncTask.log.info(log); + logBuffer.append(Time.longLog.format(Time.now())).append('[').append(level).append(']').append(' ').append(log).append('\n'); + } + + protected void log(Throwable e) { + log.error(e.getMessage(), e); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + e.printStackTrace(new PrintWriter(baos)); + logBuffer.append(Time.longLog.format(Time.now())).append("[ERROR]").append(' ').append(baos).append('\n'); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/service/implement/AsyncTaskServiceImplement.java b/src/main/java/com/imyeyu/server/modules/system/service/implement/AsyncTaskServiceImplement.java new file mode 100644 index 0000000..deaa698 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/service/implement/AsyncTaskServiceImplement.java @@ -0,0 +1,159 @@ +package com.imyeyu.server.modules.system.service.implement; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.io.IOSize; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.system.entity.AsyncTask; +import com.imyeyu.server.modules.system.mapper.AsyncTaskMapper; +import com.imyeyu.server.modules.system.service.AsyncTaskService; +import com.imyeyu.server.modules.system.vo.AsyncTaskView; +import com.imyeyu.utils.Time; +import org.springframework.beans.BeanUtils; +import org.springframework.scheduling.config.CronTask; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * 异步任务列表执行器 + * + * @author 夜雨 + * @version 2022-08-13 10:40 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AsyncTaskServiceImplement implements AsyncTaskService { + + private final AsyncTaskMapper mapper; + private final ScheduledTaskRegistrar taskRegistrar; + + /** 任务列表 */ + private final Map map = new HashMap<>(); + + @PostConstruct + private void taskPolling() { + taskRegistrar.scheduleCronTask(new CronTask(() -> { + long now = Time.now(); + + Iterator iterator = map.keySet().iterator(); + while (iterator.hasNext()) { + String uuid = iterator.next(); + + AbstractAsyncTask task = map.get(uuid); + switch (task.getStatus()) { + case IDLE -> task.startup(); + case ERROR -> { + if (Time.D * 3 < now - task.getErrorAt()) { + // 异常 3 天死亡 + task.setStatus(AsyncTask.Status.DIED); + task.setDiedAt(now); + } + } + case INTERRUPT -> { + if (Time.M * 3 < now - task.getInterruptAt()) { + // 中断 3 分钟死亡 + task.setStatus(AsyncTask.Status.DIED); + task.setDiedAt(now); + } + } + case DIED -> { + if (Time.M * 3 < now - task.getDiedAt()) { + // 死亡 3 分钟移除 + iterator.remove(); + log.info("remove died async task uuid = " + task.getUuid()); + } + } + } + } + }, "0/1 * * * * ?")); + } + + @PostConstruct + private void updateTaskRecord() { + taskRegistrar.scheduleCronTask(new CronTask(() -> { + for (Map.Entry item : map.entrySet()) { + item.getValue().setMessage(item.getValue().logBuffer.toString()); + mapper.update(item.getValue()); + } + }, "0 */5 * * * ?")); + } + + /** @param task 添加异步任务 */ + public void addAsyncTask(AbstractAsyncTask task) { + task.setCreatedAt(Time.now()); + mapper.insert(task); + + map.put(task.getUuid(), task); + } + + /** + * 获取异步任务 + * + * @param uuid 任务 UUID + * @return 异步任务 + */ + public AbstractAsyncTask getAsyncTask(String uuid) { + if (map.containsKey(uuid)) { + AbstractAsyncTask task = map.get(uuid); + task.setMessage(task.logBuffer.toString()); + return task; + } + throw new TimiException(TimiCode.RESULT_NULL).msgKey("TODO not found task"); + } + + @Override + public void start(String uuid) throws TimiException { + getAsyncTask(uuid).start(); + } + + @Override + public void pause(String uuid) throws TimiException { + getAsyncTask(uuid).pause(); + } + + @Override + public void interrupt(String uuid) throws TimiException { + getAsyncTask(uuid).interrupt(); + } + + /** @param task 移除异步任务 */ + public void removeAsyncTask(AbstractAsyncTask task) { + task.interrupt(); + map.remove(task.getUuid()); + } + + /** @return 异步任务列表 */ + public List listAll() { + return map.values().stream().sorted(Comparator.comparingLong(AbstractAsyncTask::getStartAt).reversed()).toList(); + } + + @Override + public List listAllView() { + List result = new ArrayList<>(); + List taskList = listAll(); + for (int i = 0; i < taskList.size(); i++) { + AbstractAsyncTask task = taskList.get(i); + task.setMessage(task.logBuffer.toString()); + task.setMessage(TimiJava.firstNotNull(task.getMessage(), "")); + if (IOSize.KB * 4 < task.getMessage().length()) { + task.setMessage(task.getMessage().substring((int) (task.getMessage().length() - IOSize.KB * 4))); + } + AsyncTaskView view = new AsyncTaskView(); + BeanUtils.copyProperties(task, view); + view.setNextExecuteAt(task.nextExecuteAt); + result.add(view); + } + return result; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/service/implement/FileServiceImplement.java b/src/main/java/com/imyeyu/server/modules/system/service/implement/FileServiceImplement.java new file mode 100644 index 0000000..d50e546 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/service/implement/FileServiceImplement.java @@ -0,0 +1,271 @@ +package com.imyeyu.server.modules.system.service.implement; + +import com.imyeyu.io.IO; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.server.modules.system.bean.FileSyncConfig; +import com.imyeyu.server.modules.system.bean.ServerFile; +import com.imyeyu.server.modules.system.bean.TransferFile; +import com.imyeyu.server.modules.system.entity.AsyncTask; +import com.imyeyu.server.modules.system.service.AsyncTaskService; +import com.imyeyu.server.modules.system.service.FileService; +import com.imyeyu.server.modules.system.task.async.FileSyncTask; +import com.imyeyu.server.modules.system.util.SystemAPIInterceptor; +import com.imyeyu.utils.Decoder; +import com.imyeyu.utils.OS; +import jakarta.annotation.PostConstruct; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.config.CronTask; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.stereotype.Service; + +import javax.naming.NoPermissionException; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; +import java.util.Map; + +/** + * 文件服务,不可直接使用入参路径 + * + * @author 夜雨 + * @version 2022-01-07 16:05 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class FileServiceImplement implements TimiJava, FileService { + + /** + * + * + * @author 夜雨 + * @since 2025-03-31 14:50 + */ + @Data + public static class FilterOption { + + List allow; + + List exclude; + } + + private final SettingService settingService; + private final AsyncTaskService asyncTaskService; + + private final SystemAPIInterceptor interceptor; + private final ScheduledTaskRegistrar scheduledTaskRegistrar; + + private FilterOption option; + + @PostConstruct + private void fileSyncPostConstruct() { + FileSyncConfig config = settingService.fromYaml(SettingKey.SYSTEM_FILE_SYNC, FileSyncConfig.class); + if (config.isEnable()) { + scheduledTaskRegistrar.scheduleCronTask(new CronTask(() -> { + Map tasks = config.getTasks(); + configTask: for (Map.Entry item : tasks.entrySet()) { + if (item.getValue().isEnable()) { + List runningTaskList = asyncTaskService.listAll(); + for (int i = 0; i < runningTaskList.size(); i++) { + AbstractAsyncTask runningTask = runningTaskList.get(i); + if (runningTask.getType() == AsyncTask.Type.FILE_SYNC && runningTask.getName().equals(item.getKey())) { + continue configTask; + } + } + FileSyncTask task = new FileSyncTask(item.getKey(), item.getValue()); + asyncTaskService.addAsyncTask(task); + } + } + }, config.getCron())); + } + } + + @PostConstruct + private void fileFilterPostConstruct() { + option = settingService.fromYaml(SettingKey.SYSTEM_FILE_FILTER, FilterOption.class); + } + + @Override + public boolean canAccess(String path) { + if (interceptor.isSuperKey()) { + return true; + } + for (int i = 0; i < option.allow.size(); i++) { + if (!path.startsWith(option.allow.get(i))) { + return false; + } + } + for (int i = 0; i < option.exclude.size(); i++) { + if (path.startsWith(option.exclude.get(i))) { + return false; + } + } + return true; + } + + @Override + public void checkAccessPermission(String path) { + if (canAccess(path)) { + return; + } + throw new TimiException(TimiCode.PERMISSION_ERROR).msgKey("TODO invalid permission"); + } + + @Override + public List doFilter(List list) { + list.removeIf(item -> !canAccess(item.getAbsolutePath())); + return list; + } + + @Override + public List list(String path) { + File file = new File(getPath(path)); + if (file.exists()) { + List result = new ArrayList<>(); + + File[] files = file.listFiles(); + if (files != null) { + List list = new ArrayList<>(List.of(files)); + list.sort(OS.FileSystem.COMPARATOR_FILE_NAME); + for (int i = 0; i < list.size(); i++) { + result.add(new ServerFile(list.get(i))); + } + } + return result; + } else { + throw new TimiException(TimiCode.RESULT_NULL).msgKey("文件或路径不存在"); + } + } + + @Override + public boolean isExist(String path) { + return new File(getPath(path)).exists(); + } + + @Override + public List search(String path, String keyword) { + File file = new File(getPath(path)); + if (file.exists()) { + List result = new ArrayList<>(); + // 右进左出文件夹队列,非递归深度栈遍历 + Deque directoryQueue = new ArrayDeque<>(); + directoryQueue.addLast(file); + + deque: while (!directoryQueue.isEmpty()) { + File directory = directoryQueue.pollFirst(); + File[] files = directory.listFiles(); + if (files != null) { + for (int i = 0; i < files.length; i++) { + if (files[i].getName().contains(keyword)) { + result.add(new ServerFile(files[i])); + if (100 < result.size()) { + break deque; + } + } + if (files[i].isDirectory()) { + directoryQueue.addLast(files[i]); + } + } + } + } + return result; + } else { + throw new TimiException(TimiCode.RESULT_NULL).msgKey("文件或路径不存在"); + } + } + + @Override + public File mkdir(String path, String name) { + String uri = IO.fitPath(getPath(path)) + name; + try { + return IO.dir(uri); + } catch (NoPermissionException e) { + throw new TimiException(TimiCode.PERMISSION_ERROR).msgKey("权限错误"); + } + } + + @Override + public boolean rename(String from, String to) { + File source = new File(getPath(from)); + checkAccessPermission(source.getAbsolutePath()); + if (source.exists()) { + return source.renameTo(new File(getPath(to))); + } + return false; + } + + @Override + public InputStream getInputStream(String path) { + try { + return IO.getInputStream(getPath(path)); + } catch (FileNotFoundException e) { + throw new TimiException(TimiCode.RESULT_NULL).msgKey("找不到文件"); + } + } + + @Override + public void upload(TransferFile file) { + String path = getPath(Decoder.url(file.getPath())); + if (file.getName() == null) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:name"); + } + String fileName = Decoder.url(file.getName()); + if (!OS.isValidFileName(path)) { + throw new TimiException(TimiCode.ARG_BAD).msgKey("路径名称不合法:" + path); + } + if (!OS.isValidFileName(fileName)) { + throw new TimiException(TimiCode.ARG_BAD).msgKey("文件名称不合法:" + fileName); + } + if (file.getLength() == file.getFile().getSize()) { + try { + toFile(new File(path + fileName), file.getFile().getInputStream()); + } catch (IOException e) { + log.error("get request input stream error", e); + throw new TimiException(TimiCode.ARG_BAD).msgKey("获取文件输入流失败"); + } + } else { + log.error("request submit request size: {}, receive request size: {}", file.getLength(), file.getFile().getSize()); + throw new TimiException(TimiCode.ERROR).msgKey("不完整上传文件,已忽略"); + } + } + + @Override + public void toFile(File file, InputStream is) { + try { + IO.toFile(file, is); + } catch (IOException e) { + throw new TimiException(TimiCode.ERROR); + } catch (NoPermissionException e) { + throw new TimiException(TimiCode.PERMISSION_ERROR); + } + } + + @Override + public void destroy(String... list) { + for (int i = 0; i < list.length; i++) { + File file = new File(list[i]); + if (canAccess(file) && file.exists() && file.canWrite()) { + IO.destroy(file); + } + } + } + + private String getPath(String path) { + String basePath = settingService.getAsString(SettingKey.SYSTEM_FILE_BASE); + if (TimiJava.isEmpty(path)) { + return basePath; + } + return basePath + path; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/service/implement/SystemServiceImplement.java b/src/main/java/com/imyeyu/server/modules/system/service/implement/SystemServiceImplement.java new file mode 100644 index 0000000..d7d7ee7 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/service/implement/SystemServiceImplement.java @@ -0,0 +1,73 @@ +package com.imyeyu.server.modules.system.service.implement; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.io.IO; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.TimiServerAPI; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.server.modules.system.service.SystemService; +import com.imyeyu.utils.OS; +import org.springframework.boot.SpringApplication; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import javax.naming.NoPermissionException; +import java.io.IOException; + +/** + * @author 夜雨 + * @version 2024-03-13 00:45 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SystemServiceImplement implements SystemService { + + private final SettingService settingService; + + @Override + public void update(MultipartFile file) { + try { + IO.toFile(IO.file("timi-server-api.jar"), file.getInputStream()); + } catch (NoPermissionException | IOException e) { + log.error("update core error", e); + throw new TimiException(TimiCode.ERROR).msgKey("TODO update core error"); + } + } + + @Override + public void restore() { + try { + IO.copy(IO.file("default.jar"), IO.file("timi-server-api.jar").getAbsolutePath()); + } catch (NoPermissionException | IOException e) { + log.error("restore core error", e); + throw new TimiException(TimiCode.ERROR).msgKey("TODO restore core error"); + } + } + + @Override + public void shutdown() { + if (TimiServerAPI.applicationContext instanceof AbstractApplicationContext ctx) { + ctx.close(); + } + } + + @Async + @Override + public void reboot() { + if (TimiServerAPI.applicationContext instanceof AbstractApplicationContext ctx) { + String command = settingService.getAsString(SettingKey.SYSTEM_REBOOT_COMMAND); + if (TimiJava.isEmpty(command)) { + throw new TimiException(TimiCode.ERROR_NPE_VARIABLE).msgKey("TODO not support reboot"); + } + OS.runAfterShutdown(command); + SpringApplication.exit(ctx); + } + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/service/implement/TerminalServiceImplement.java b/src/main/java/com/imyeyu/server/modules/system/service/implement/TerminalServiceImplement.java new file mode 100644 index 0000000..3293949 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/service/implement/TerminalServiceImplement.java @@ -0,0 +1,132 @@ +package com.imyeyu.server.modules.system.service.implement; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.CallbackArg; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.server.modules.system.bean.TerminalPipe; +import com.imyeyu.server.modules.system.service.TerminalService; +import com.imyeyu.server.modules.system.vo.terminal.ExecCommand; +import com.imyeyu.spring.TimiSpring; +import com.imyeyu.utils.OS; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** + * 指令服务 + * + * @author 夜雨 + * @version 2022-07-22 10:30 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class TerminalServiceImplement implements TerminalService, TimiJava, OS.FileSystem { + + /** Linux 控制台 */ + private static final String STARTUP_LINUX = "/bin/bash"; + + /** Windows 控制台 */ + private static final String STARTUP_WINDOWS = "cmd"; + + private final SettingService settingService; + + /** 会话管道映射 Map<SessionID, 管道>,此 SessionID 由 {@link #create()} 创建,非 HTTP SessionID */ + private final Map pipes = new HashMap<>(); + private final Set execLogFilters = new HashSet<>(); + + @PostConstruct + private void postConstruct() { + execLogFilters.addAll(List.of(settingService.getAsString(SettingKey.SYSTEM_TERMINAL_FILTERS).split(","))); + } + + @Override + public Map listTerminalPipe() { + return pipes; + } + + @Override + public String create() { + try { + String id = UUID.randomUUID().toString(); + pipes.put(id, new TerminalPipe(Runtime.getRuntime().exec(OS.IS_WINDOWS ? STARTUP_WINDOWS : STARTUP_LINUX))); + return id; + } catch (IOException e) { + throw new TimiException(TimiCode.ERROR).msgKey("创建控制台会话失败:" + e.getLocalizedMessage()); + } + } + + @Override + public boolean isAlive(String sessionID) { + return pipes.get(sessionID) != null && pipes.get(sessionID).getStatus() != TerminalPipe.Status.DIED; + } + + @Override + public String pathFill(String path) { + int splitAt = path.lastIndexOf(SEP); + if (splitAt == -1) { + return ""; + } + String parent = path.substring(0, splitAt + 1); + File file = new File(parent); + if (!file.exists()) { + return ""; + } + File[] files = file.listFiles(); + if (files == null) { + return ""; + } + String startWith = path.substring(splitAt + 1); + for (int i = 0; i < files.length; i++) { + if (files[i].getName().startsWith(startWith)) { + if (files[i].isDirectory()) { + return files[i].getName().substring(startWith.length()) + SEP; + } else { + return files[i].getName().substring(startWith.length()); + } + } + } + return ""; + } + + @Override + public void exec(ExecCommand execCommand, CallbackArg callback) { + try { + if (!execLogFilters.contains(execCommand.getCommand())) { + log.info("IP: {} -> [{}]# {}", TimiSpring.getRequestIP(), execCommand.getSessionId(), execCommand.getCommand()); + } + TerminalPipe pipe = pipes.get(execCommand.getSessionId()); + if (pipe == null) { + throw new TimiException(TimiCode.RESULT_NULL).msgKey("控制台会话不存在"); + } + synchronized (pipe.getLock()) { + pipe.exec(callback, execCommand.getCommand()); + pipe.getLock().wait(); + } + } catch (InterruptedException e) { + throw new TimiException(TimiCode.ERROR).msgKey("指令执行失败:" + e.getMessage()); + } + } + + @Override + public void close(String sessionID) { + TerminalPipe pipe = pipes.get(sessionID); + if (pipe == null) { + throw new TimiException(TimiCode.RESULT_NULL).msgKey("控制台会话不存在"); + } + pipe.destroy(); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/task/ServerStatusTask.java b/src/main/java/com/imyeyu/server/modules/system/task/ServerStatusTask.java new file mode 100644 index 0000000..957fbdc --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/task/ServerStatusTask.java @@ -0,0 +1,235 @@ +package com.imyeyu.server.modules.system.task; + +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.utils.OS; +import com.imyeyu.utils.Time; +import com.imyeyu.java.TimiJava; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.server.modules.system.bean.ServerStatus; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.scheduling.support.CronTrigger; +import org.springframework.stereotype.Service; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; +import oshi.hardware.GlobalMemory; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.hardware.NetworkIF; +import oshi.software.os.OSFileStore; +import oshi.software.os.OperatingSystem; + +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.util.Deque; +import java.util.List; + +/** + * 服务器状态收集任务 + * + * @author 夜雨 + * @version 2022-01-31 15:18 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ServerStatusTask implements SchedulingConfigurer, TimiJava { + + private final ServerStatus status; + private final SettingService settingService; + + private GlobalMemory globalMemory; // 内存 + private MemoryMXBean jvmMemory; // JVM 内存 + private OperatingSystem os; // 操作系统 + private CentralProcessor processor; // 中央处理器 + private HardwareAbstractionLayer hardware; // 硬件 + + private long[] lastCPUTicks; // CPU 上一时刻状态 + private long lastLinuxAPIMemoryUsed; // 上一周期 JVM 内存大小 + + @Override + public void configureTasks(@NotNull ScheduledTaskRegistrar taskRegistrar) { + lastLinuxAPIMemoryUsed = -1; + + // 系统信息 + SystemInfo system = new SystemInfo(); + + // 硬件信息 + hardware = system.getHardware(); + + // 操作系统 + os = system.getOperatingSystem(); + + // ---------- 静态数据 ---------- + + // 系统 + status.getOs().setName(OS.NAME); + status.getOs().setBootAt(os.getSystemBootTime() * 1000); + + // JVM + jvmMemory = ManagementFactory.getMemoryMXBean(); + status.getJvm().setBootAt(Time.now()); + status.getJvm().setName(System.getProperty("java.vm.name")); + status.getJvm().setVersion(System.getProperty("java.version")); + + // CPU + processor = hardware.getProcessor(); + status.getCpu().setName(processor.getProcessorIdentifier().getName().trim()); + status.getCpu().setCoreCount(processor.getPhysicalProcessorCount()); + status.getCpu().setLogicalCount(processor.getLogicalProcessorCount()); + + // 内存 + globalMemory = hardware.getMemory(); + status.getMemory().setSize(globalMemory.getTotal()); + status.getMemory().setSwapSize(globalMemory.getVirtualMemory().getSwapTotal()); + + // 网卡 + List networkIFs = hardware.getNetworkIFs(); + { + NetworkIF networkIF; + boolean isFound = false; + for (int i = 0; i < networkIFs.size(); i++) { + networkIF = networkIFs.get(i); + if (networkIF.getMacaddr().equals(settingService.getAsString(SettingKey.SYSTEM_STATUS_NETWORK_MAC))) { + status.getNetwork().setMac(networkIF.getMacaddr()); + + status.getNetwork().setRecvTotal(networkIF.getBytesRecv()); + status.getNetwork().setSentTotal(networkIF.getBytesSent()); + + isFound = true; + break; + } + } + if (!isFound) { + log.error("not found setting networkIF MAC: %s" + settingService.getAsString(SettingKey.SYSTEM_STATUS_NETWORK_MAC)); + for (int i = 0; i < networkIFs.size(); i++) { + log.info("Network Interface: {} -> {}", networkIFs.get(i).getMacaddr(), networkIFs.get(i).getDisplayName()); + } + } + } + + taskRegistrar.addTriggerTask(() -> { + long now = Time.now(); + + // ---------- JVM 内存 ---------- + long nowLinuxAPIMemoryUsed = jvmMemory.getHeapMemoryUsage().getUsed(); + long linuxAPIMemoryDiff = Math.abs(nowLinuxAPIMemoryUsed - lastLinuxAPIMemoryUsed); + lastLinuxAPIMemoryUsed = nowLinuxAPIMemoryUsed; + status.getJvm().getMemory().setInit(jvmMemory.getHeapMemoryUsage().getInit()); + status.getJvm().getMemory().setMax(jvmMemory.getHeapMemoryUsage().getMax()); + putDeque(status.getJvm().getMemory().getUsed(), nowLinuxAPIMemoryUsed); + putDeque(status.getJvm().getMemory().getCommitted(), jvmMemory.getHeapMemoryUsage().getCommitted()); + + // ---------- JVM GC ---------- + long gcSyncCycles = 0; + long gcSyncCyclesTime = 0; + long gcPauses = 0; + long gcPausesTime = 0; + for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) { + gcSyncCycles += gc.getCollectionCount(); + gcSyncCyclesTime += gc.getCollectionTime(); + switch (gc.getName()) { + case "ZGC Cycles" -> { + gcSyncCycles += gc.getCollectionCount(); + gcSyncCyclesTime += gc.getCollectionTime(); + } + case "ZGC Pauses" -> { + gcPauses += gc.getCollectionCount(); + gcPausesTime += gc.getCollectionTime(); + if (status.getJvm().getZgc().getPauses() < gcPauses) { + // 发生 GC 回收 + status.getJvm().getZgc().setLastPauseAt(now); + status.getJvm().getZgc().setLastRecoverySize(linuxAPIMemoryDiff); + } + } + } + } + putDeque(status.getJvm().getZgc().getSyncCyclesTime(), gcSyncCyclesTime - status.getJvm().getZgc().getSyncCyclesTimeTotal()); + putDeque(status.getJvm().getZgc().getPausesTime(), gcPausesTime - status.getJvm().getZgc().getPausesTimeTotal()); + status.getJvm().getZgc().setSyncCycles(gcSyncCycles); + status.getJvm().getZgc().setSyncCyclesTimeTotal(gcSyncCyclesTime); + status.getJvm().getZgc().setPauses(gcPauses); + status.getJvm().getZgc().setPausesTimeTotal(gcPausesTime); + + // ---------- CPU ---------- + if (lastCPUTicks != null) { + long[] ticks = processor.getSystemCpuLoadTicks(); + + long user = ticks[CentralProcessor.TickType.USER.getIndex()] - lastCPUTicks[CentralProcessor.TickType.USER.getIndex()]; + long nice = ticks[CentralProcessor.TickType.NICE.getIndex()] - lastCPUTicks[CentralProcessor.TickType.NICE.getIndex()]; + long sys = ticks[CentralProcessor.TickType.SYSTEM.getIndex()] - lastCPUTicks[CentralProcessor.TickType.SYSTEM.getIndex()]; + long idle = ticks[CentralProcessor.TickType.IDLE.getIndex()] - lastCPUTicks[CentralProcessor.TickType.IDLE.getIndex()]; + long ioWait = ticks[CentralProcessor.TickType.IOWAIT.getIndex()] - lastCPUTicks[CentralProcessor.TickType.IOWAIT.getIndex()]; + long irq = ticks[CentralProcessor.TickType.IRQ.getIndex()] - lastCPUTicks[CentralProcessor.TickType.IRQ.getIndex()]; + long softIRQ = ticks[CentralProcessor.TickType.SOFTIRQ.getIndex()] - lastCPUTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()]; + long steal = ticks[CentralProcessor.TickType.STEAL.getIndex()] - lastCPUTicks[CentralProcessor.TickType.STEAL.getIndex()]; + long total = user + nice + sys + idle + ioWait + irq + softIRQ + steal; + + putDeque(status.getCpu().getSystem(), 100D * sys / total); + putDeque(status.getCpu().getUsed(), 100 - 100D * idle / total); + } + lastCPUTicks = processor.getSystemCpuLoadTicks(); + status.getCpu().setTemperature(hardware.getSensors().getCpuTemperature()); + + // ---------- 内存 ---------- + putDeque(status.getMemory().getUsed(), globalMemory.getTotal() - globalMemory.getAvailable()); + putDeque(status.getMemory().getSwapUsed(), globalMemory.getVirtualMemory().getSwapUsed()); + + // ---------- 网络 ---------- + networkIFs.clear(); + networkIFs.addAll(hardware.getNetworkIFs()); + NetworkIF networkIF; + for (int i = 0; i < networkIFs.size(); i++) { + networkIF = networkIFs.get(i); + if (networkIF.getMacaddr().equals(settingService.getAsString(SettingKey.SYSTEM_STATUS_NETWORK_MAC))) { + long recv = networkIF.getBytesRecv() - status.getNetwork().getRecvTotal(); + long sent = networkIF.getBytesSent() - status.getNetwork().getSentTotal(); + status.getNetwork().setRecvNow(recv / (settingService.getAsInt(SettingKey.SYSTEM_STATUS_RATE) / 1000)); + status.getNetwork().setSentNow(sent / (settingService.getAsInt(SettingKey.SYSTEM_STATUS_RATE) / 1000)); + status.getNetwork().setRecvTotal(networkIF.getBytesRecv()); + status.getNetwork().setSentTotal(networkIF.getBytesSent()); + + putDeque(status.getNetwork().getRecv(), recv); + putDeque(status.getNetwork().getSent(), sent); + break; + } + } + + // ---------- 磁盘分区 ---------- + ServerStatus.Partition partition; + status.getPartitions().clear(); + // 分区从文件系统获取,而非物理分区 + List fileStores = os.getFileSystem().getFileStores(); + for (OSFileStore fileStore : fileStores) { + partition = new ServerStatus.Partition(); + partition.setUuid(fileStore.getUUID()); + partition.setPath(fileStore.getMount()); + partition.setType(fileStore.getType()); + partition.setUsed(fileStore.getTotalSpace() - fileStore.getUsableSpace()); + partition.setTotal(fileStore.getTotalSpace()); + + status.getPartitions().add(partition); + } + + // ---------- 更新时轴 ---------- + putDeque(status.getUpdateAxis(), Time.now()); + }, tc -> new CronTrigger("0/%s * * * * ?".formatted(settingService.getAsInt(SettingKey.SYSTEM_STATUS_RATE) / 1000)).nextExecution(tc)); + } + + /** + * 有所限制地添加队列数据,达到配置 {@link SettingKey#SYSTEM_STATUS_LIMIT} 个时移除最旧的 + * + * @param deque 队列 + * @param t 数据 + * @param 数据类型 + */ + private void putDeque(Deque deque, T t) { + deque.addLast(t); + if (settingService.getAsInt(SettingKey.SYSTEM_STATUS_LIMIT) < deque.size()) { + deque.pollFirst(); + } + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/task/TerminalTask.java b/src/main/java/com/imyeyu/server/modules/system/task/TerminalTask.java new file mode 100644 index 0000000..8146a2a --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/task/TerminalTask.java @@ -0,0 +1,45 @@ +package com.imyeyu.server.modules.system.task; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.server.modules.system.bean.TerminalPipe; +import com.imyeyu.server.modules.system.service.TerminalService; +import com.imyeyu.utils.Time; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + +/** + * 控制台会话存活轮询 + * + * @author 夜雨 + * @version 2022-07-25 10:27 + */ +@Slf4j +@Configuration +@EnableScheduling +@RequiredArgsConstructor +public class TerminalTask { + + private final SettingService settingService; + private final TerminalService service; + + @Scheduled(fixedRate = Time.M) + private void run() { + service.listTerminalPipe().entrySet().removeIf(entry -> { + if (entry.getValue().getStatus() == TerminalPipe.Status.DIED) { + log.info("kill the died terminal session: " + entry.getKey()); + return true; + } + long diedAt = entry.getValue().getLastExecAt() + Time.M * settingService.getAsInt(SettingKey.SYSTEM_TERMINAL_TTL); + if (diedAt < Time.now()) { + entry.getValue().destroy(); + log.info("kill the timeout terminal session [DIED at: {}]: {}", Time.toDateTime(diedAt), entry.getKey()); + return true; + } + return false; + }); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/task/async/DebugAsyncTask.java b/src/main/java/com/imyeyu/server/modules/system/task/async/DebugAsyncTask.java new file mode 100644 index 0000000..6156227 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/task/async/DebugAsyncTask.java @@ -0,0 +1,34 @@ +package com.imyeyu.server.modules.system.task.async; + +import ch.qos.logback.classic.Level; +import com.imyeyu.server.modules.system.entity.AsyncTask; +import com.imyeyu.server.modules.system.service.implement.AbstractAsyncTask; + +/** + * 用于调试的虚拟异步任务 + * + * @author 夜雨 + * @version 2022-08-15 09:45 + */ +public class DebugAsyncTask extends AbstractAsyncTask { + + private final int second; + + public DebugAsyncTask(int second) { + this.second = second; + type = AsyncTask.Type.DEBUG; + } + + @Override + protected void run() throws Exception { + for (int i = 0, l = second + 1; i < l; i++) { + pauseInterruptHandle(); + + log(Level.INFO, "Debug " + i); + progress = 1D * i++ / second; + + // 执行 + Thread.sleep(1000); + } + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/task/async/FileCalcSizeAsyncTask.java b/src/main/java/com/imyeyu/server/modules/system/task/async/FileCalcSizeAsyncTask.java new file mode 100644 index 0000000..da3889b --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/task/async/FileCalcSizeAsyncTask.java @@ -0,0 +1,54 @@ +package com.imyeyu.server.modules.system.task.async; + +import ch.qos.logback.classic.Level; +import com.imyeyu.server.modules.system.entity.AsyncTask; +import com.imyeyu.server.modules.system.service.implement.AbstractAsyncTask; + +import java.io.File; +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * 计算文件夹大小(无法计算进度) + * + * @author 夜雨 + * @version 2022-08-21 21:36 + */ +public class FileCalcSizeAsyncTask extends AbstractAsyncTask { + + private final File file; + + public FileCalcSizeAsyncTask(File file) { + this.file = file; + type = AsyncTask.Type.FILE_CALC_SIZE; + } + + @Override + protected void run() throws InterruptedException { + long total = 0; + + if (file != null && file.exists() && file.isDirectory()) { + // 左出右进,文件夹队列 + Deque deque = new ArrayDeque<>(); + deque.addLast(file); + + while (!deque.isEmpty()) { + File poll = deque.pollFirst(); + pauseInterruptHandle(); + + if (poll.isDirectory()) { + File[] files = poll.listFiles(); + if (files != null) { + for (int i = 0; i < files.length; i++) { + deque.addLast(files[i]); + } + } + } else { + total += poll.length(); + log(Level.INFO, String.valueOf(total)); + } + } + } + progress = 1; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/task/async/FileCopyAsyncTask.java b/src/main/java/com/imyeyu/server/modules/system/task/async/FileCopyAsyncTask.java new file mode 100644 index 0000000..9acdefb --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/task/async/FileCopyAsyncTask.java @@ -0,0 +1,73 @@ +package com.imyeyu.server.modules.system.task.async; + +import ch.qos.logback.classic.Level; +import com.imyeyu.io.IO; +import com.imyeyu.server.modules.system.entity.AsyncTask; +import com.imyeyu.server.modules.system.service.implement.AbstractAsyncTask; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 文件复制 + * + * @author 夜雨 + * @version 2022-08-15 16:55 + */ +public class FileCopyAsyncTask extends AbstractAsyncTask { + + private final String to; + private final List fromList; + + public FileCopyAsyncTask(List fromList, String to) { + this.fromList = fromList; + this.to = to; + + type = AsyncTask.Type.FILE_COPY; + } + + @Override + protected void run() throws Exception { + // 准备文件 Map<文件, 深度路径>,深度路径含文件名 + Map fromList = new HashMap<>(); + File file; + for (int i = 0; i < this.fromList.size(); i++) { + pauseInterruptHandle(); + + file = new File(this.fromList.get(i)); + if (file.exists()) { + if (file.isFile()) { + fromList.put(file, file.getName()); + } else { + String basePath = file.getParentFile().getAbsolutePath(); + List files = IO.listFile(file); + for (int j = 0; j < files.size(); j++) { + fromList.put(files.get(j), files.get(j).getAbsolutePath().substring(basePath.length())); + } + } + } + } + // 复制文件 + int total = fromList.size(); + int i = 0; + for (Map.Entry item : fromList.entrySet()) { + pauseInterruptHandle(); + + log(Level.INFO, item.getKey().getName()); + // 执行复制 + { + File toFile = new File(IO.fitPath(to) + item.getValue()); + IO.dir(toFile.getParent()); + Files.copy(item.getKey().toPath(), toFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + + // 更新进度 + progress = 1D * ++i / total; + } + progress = 1; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/task/async/FileMoveAsyncTask.java b/src/main/java/com/imyeyu/server/modules/system/task/async/FileMoveAsyncTask.java new file mode 100644 index 0000000..b0dae1d --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/task/async/FileMoveAsyncTask.java @@ -0,0 +1,85 @@ +package com.imyeyu.server.modules.system.task.async; + +import ch.qos.logback.classic.Level; +import com.imyeyu.io.IO; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.system.entity.AsyncTask; +import com.imyeyu.server.modules.system.service.implement.AbstractAsyncTask; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 文件批量移动异步任务 + * + * @author 夜雨 + * @version 2022-08-13 14:50 + */ +public class FileMoveAsyncTask extends AbstractAsyncTask { + + private final String to; + private final List fromList; + + public FileMoveAsyncTask(List fromList, String to) { + this.fromList = fromList; + this.to = to; + + type = AsyncTask.Type.FILE_MOVE; + } + + @Override + protected void run() throws Exception { + // 准备文件 Map<文件, 深度路径>,深度路径含文件名 + Map fromList = new HashMap<>(); + File file; + for (int i = 0; i < this.fromList.size(); i++) { + pauseInterruptHandle(); + + file = new File(this.fromList.get(i)); + if (file.exists()) { + if (file.isFile()) { + fromList.put(file, file.getName()); + } else { + String basePath = file.getParentFile().getAbsolutePath(); + List files = IO.listFile(file); + for (int j = 0; j < files.size(); j++) { + fromList.put(files.get(j), files.get(j).getAbsolutePath().substring(basePath.length())); + } + } + } + } + // 移动文件 + int total = fromList.size(); + int i = 0; + for (Map.Entry item : fromList.entrySet()) { + pauseInterruptHandle(); + + log(Level.INFO, item.getKey().getName()); + // 执行移动 + { + File toFile = new File(IO.fitPath(to) + item.getValue()); + IO.dir(toFile.getParent()); + if (!item.getKey().renameTo(toFile)) { + throw new TimiException(TimiCode.ERROR).msgKey("移动失败:" + item.getKey().getAbsolutePath() + " -> " + toFile.getAbsolutePath()); + } + } + + // 更新进度 + progress = 1D * ++i / total; + } + progress = 1; + + // 删除已移动文件夹 + for (int j = 0; j < this.fromList.size(); j++) { + pauseInterruptHandle(); + + file = new File(this.fromList.get(j)); + if (file.exists()) { + IO.destroy(file); + } + } + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/task/async/FileSyncTask.java b/src/main/java/com/imyeyu/server/modules/system/task/async/FileSyncTask.java new file mode 100644 index 0000000..6dde900 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/task/async/FileSyncTask.java @@ -0,0 +1,150 @@ +package com.imyeyu.server.modules.system.task.async; + +import ch.qos.logback.classic.Level; +import com.imyeyu.io.IO; +import com.imyeyu.io.TreeFile; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.system.bean.FileSyncConfig; +import com.imyeyu.server.modules.system.entity.AsyncTask; +import com.imyeyu.server.modules.system.service.implement.AbstractAsyncTask; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 文件同步任务 + * + * @author 夜雨 + * @version 2024-06-16 08:36 + */ +public class FileSyncTask extends AbstractAsyncTask { + + private final FileSyncConfig.Task config; + + public FileSyncTask(String name, FileSyncConfig.Task config) { + this.config = config; + + super.name = name; + isPeriodical = true; + cron = config.getCron(); + type = AsyncTask.Type.FILE_SYNC; + + List targetList = config.getTargets(); + String sourceAbsPath = new File(config.getSource()).getAbsolutePath(); + for (int i = 0; i < targetList.size(); i++) { + File target = new File(targetList.get(i)); + if (target.getAbsolutePath().startsWith(sourceAbsPath)) { + throw new TimiException(TimiCode.ERROR, "invalid target:%s that is in source:%s".formatted(target.getAbsolutePath(), sourceAbsPath)); + } + } + } + + @Override + protected void run() throws Exception { + // 扫描 + status = AsyncTask.Status.RUNNING; + log(Level.INFO, "building file tree"); + // Map<目标, 来源> + Map copyMap = new HashMap<>(); + List mkdirList = new ArrayList<>(); + List destoryList = new ArrayList<>(); + List targetList = config.getTargets(); + List targetTreeFileList = new ArrayList<>(); + for (int i = 0; i < targetList.size(); i++) { + targetTreeFileList.add(new TreeFile(targetList.get(i))); + } + { + log(Level.INFO, "scanning path:%s".formatted(new File(config.getSource()).getAbsolutePath())); + TreeFile sourceTreeFile = new TreeFile(config.getSource()); + sourceTreeFile.foreach((relPath, sourceFile) -> { + for (int i = 0; i < targetTreeFileList.size(); i++) { +// pauseInterruptHandle(); + + File targetFile = new File(IO.fitPath(targetTreeFileList.get(i).getAbsolutePath()) + relPath); + if (sourceFile.isFile()) { + if (!targetFile.exists() || targetFile.length() != sourceFile.length()) { + // 复制 + if (config.isUseMD5Compare() && !IO.md5(targetFile).equals(IO.md5(sourceFile))) { + copyMap.put(targetFile, sourceFile); + } else { + copyMap.put(targetFile, sourceFile); + } + } + } else { + if (!targetFile.exists()) { + // 新建文件夹 + mkdirList.add(targetFile); + } + } + } + }); + // 删除 + for (int i = 0; i < targetTreeFileList.size(); i++) { + TreeFile targetTreeFile = targetTreeFileList.get(i); + targetTreeFile.foreach((relPath, targetFile) -> { +// pauseInterruptHandle(); + + File sourceFile = new File(IO.fitPath(config.getSource()) + relPath); + if (!sourceFile.exists()) { + destoryList.add(targetFile); + } + }); + } + } + // 执行 + { + // 10% 进度用于操作创建文件夹和删除文件 + int easyActionTTL = mkdirList.size() + destoryList.size(); + int easyAction = 0; + for (int j = 0; j < mkdirList.size(); j++) { + pauseInterruptHandle(); + + log(Level.INFO, "making dir:%s".formatted(mkdirList.get(j).getAbsolutePath())); + IO.dir(mkdirList.get(j).getAbsolutePath()); + progress = 1D * ++easyAction / easyActionTTL * .1; + } + for (int j = 0; j < destoryList.size(); j++) { + pauseInterruptHandle(); + + log(Level.INFO, "destroying file:%s".formatted(destoryList.get(j).getAbsolutePath())); + IO.destroy(destoryList.get(j)); + progress = 1D * ++easyAction / easyActionTTL * .1; + } + progress = .1; + + // 复制文件 + long copySizeTTL = 0; + long copySize = 0; + List copyList = new ArrayList<>(copyMap.values()); + for (int i = 0; i < copyList.size(); i++) { + copySizeTTL += copyList.get(i).length(); + } + for (Map.Entry item : copyMap.entrySet()) { + pauseInterruptHandle(); + + log(Level.INFO, "copping file:%s to %s".formatted(item.getValue().getAbsolutePath(), item.getKey().getAbsolutePath())); + final long fileSize = item.getValue().length(); + final long finalCopySize = copySize; + final long finalCopySizeTTL = copySizeTTL; + IO.copy(item.getValue(), item.getKey().getParentFile().getAbsolutePath(), new IO.OnWriteCallback() { + + @Override + public boolean handler(long total, long now) { + progress = .1 + (1D * finalCopySize + total) / finalCopySizeTTL * .9; + return true; + } + }); + copySize += fileSize; + } + } + progress = 1; + log(Level.INFO, "synced"); + synchronized (this) { + wait(3000); + } + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/task/async/FileTarAsyncTask.java b/src/main/java/com/imyeyu/server/modules/system/task/async/FileTarAsyncTask.java new file mode 100644 index 0000000..5ead7c7 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/task/async/FileTarAsyncTask.java @@ -0,0 +1,95 @@ +package com.imyeyu.server.modules.system.task.async; + +import ch.qos.logback.classic.Level; +import com.imyeyu.io.IO; +import com.imyeyu.server.modules.system.entity.AsyncTask; +import com.imyeyu.server.modules.system.service.implement.AbstractAsyncTask; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 压缩 TAR + * // TODO 可选是否压缩 + * + * @author 夜雨 + * @version 2022-08-16 16:54 + */ +public class FileTarAsyncTask extends AbstractAsyncTask { + + private final String to; + private final List fromList; + + public FileTarAsyncTask(List fromList, String to) { + this.fromList = fromList; + this.to = to; + + type = AsyncTask.Type.FILE_TAR; + } + + @Override + protected void run() throws Exception { + // 准备文件 Map<文件, 深度路径>,深度路径含文件名 + Map fromList = new HashMap<>(); + File file; + for (int i = 0; i < this.fromList.size(); i++) { + pauseInterruptHandle(); + + file = new File(this.fromList.get(i)); + if (file.exists()) { + if (file.isFile()) { + fromList.put(file, file.getName()); + } else { + String basePath = file.getParentFile().getAbsolutePath(); + List files = IO.listFile(file); + for (int j = 0; j < files.size(); j++) { + fromList.put(files.get(j), files.get(j).getAbsolutePath().substring(basePath.length())); + } + } + } + } + + // 压缩文件 + int total = fromList.size(); + int i = 0; + + FileOutputStream fos = new FileOutputStream(to); + BufferedOutputStream bos = new BufferedOutputStream(fos); + GzipCompressorOutputStream gcos = new GzipCompressorOutputStream(bos); + TarArchiveOutputStream taos = new TarArchiveOutputStream(gcos); + try { + + TarArchiveEntry tarEntry; + for (Map.Entry item : fromList.entrySet()) { + pauseInterruptHandle(); + + log(Level.INFO, item.getKey().getName()); + // 执行压缩 + { + tarEntry = new TarArchiveEntry(item.getKey(), item.getValue()); + taos.putArchiveEntry(tarEntry); + taos.write(IO.toBytes(item.getKey())); + taos.closeArchiveEntry(); + } + + // 更新进度 + progress = 1D * ++i / total; + } + progress = 1; + + } finally { + taos.finish(); + taos.close(); + gcos.close(); + bos.close(); + fos.close(); + } + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/task/async/FileUnTarAsyncTask.java b/src/main/java/com/imyeyu/server/modules/system/task/async/FileUnTarAsyncTask.java new file mode 100644 index 0000000..b2e4aaa --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/task/async/FileUnTarAsyncTask.java @@ -0,0 +1,111 @@ +package com.imyeyu.server.modules.system.task.async; + +import ch.qos.logback.classic.Level; +import com.imyeyu.io.IO; +import com.imyeyu.server.modules.system.entity.AsyncTask; +import com.imyeyu.server.modules.system.service.implement.AbstractAsyncTask; +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +/** + * 解压 TAR + * + * @author 夜雨 + * @version 2022-08-16 16:54 + */ +public class FileUnTarAsyncTask extends AbstractAsyncTask { + + private final File tarFile; + private final String unTarTo; + + public FileUnTarAsyncTask(File tarFile, String unTarTo) { + this.tarFile = tarFile; + this.unTarTo = unTarTo; + + type = AsyncTask.Type.FILE_UNTAR; + } + + @Override + protected void run() throws Exception { + int total = getTotal(); + + // 解压 + FileInputStream fis = new FileInputStream(tarFile); + GzipCompressorInputStream gcis = null; + TarArchiveInputStream tais; + if (tarFile.getName().endsWith("gz")) { + gcis = new GzipCompressorInputStream(fis); + tais = new TarArchiveInputStream(gcis); + } else { + tais = new TarArchiveInputStream(fis); + } + try { + TarArchiveEntry entry; + + int i = 0; + while ((entry = (TarArchiveEntry) tais.getNextEntry()) != null) { + pauseInterruptHandle(); + + String path = IO.fitPath(unTarTo) + entry.getName(); + + log(Level.INFO, path); + // 执行解压 + if (entry.isDirectory()) { + IO.dir(path); + } else { + IO.toFile(IO.file(path), tais); + // 更新进度(total 只包含文件) + progress = 1D * ++i / total; + } + } + progress = 1; + + } finally { + tais.close(); + if (gcis != null) { + gcis.close(); + } + fis.close(); + } + } + + private int getTotal() throws IOException, InterruptedException { + int total = 0; + { + // 统计文件数量 + FileInputStream fis = new FileInputStream(tarFile); + GzipCompressorInputStream gcis = null; + TarArchiveInputStream tais; + if (tarFile.getName().endsWith("gz")) { + gcis = new GzipCompressorInputStream(fis); + tais = new TarArchiveInputStream(gcis); + } else { + tais = new TarArchiveInputStream(fis); + } + + try { + ArchiveEntry entry; + while ((entry = tais.getNextEntry()) != null) { + pauseInterruptHandle(); + + if (!entry.isDirectory()) { + ++total; + } + } + } finally { + tais.close(); + if (gcis != null) { + gcis.close(); + } + fis.close(); + } + } + return total; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/task/async/FileUnZipAsyncTask.java b/src/main/java/com/imyeyu/server/modules/system/task/async/FileUnZipAsyncTask.java new file mode 100644 index 0000000..4e57302 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/task/async/FileUnZipAsyncTask.java @@ -0,0 +1,93 @@ +package com.imyeyu.server.modules.system.task.async; + +import ch.qos.logback.classic.Level; +import com.imyeyu.io.IO; +import com.imyeyu.java.TimiJava; +import com.imyeyu.server.modules.system.entity.AsyncTask; +import com.imyeyu.server.modules.system.service.implement.AbstractAsyncTask; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.util.Enumeration; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * 解压缩 ZIP + * + * @author 夜雨 + * @version 2022-08-15 16:58 + */ +public class FileUnZipAsyncTask extends AbstractAsyncTask implements TimiJava { + + private final File zipFile; + private final String unZipTo; + + public FileUnZipAsyncTask(File zipFile, String unZipTo) { + this.zipFile = zipFile; + this.unZipTo = unZipTo; + + type = AsyncTask.Type.FILE_UNZIP; + } + + @Override + @SuppressWarnings("unchecked") + protected void run() throws Exception { + ZipFile zip = new ZipFile(zipFile); + Enumeration entries = (Enumeration) zip.entries(); + + // 统计数量 + int total = 0; + + while (entries.hasMoreElements()) { + if (!entries.nextElement().isDirectory()) { + ++total; + } + } + + // 解压 + ZipEntry entry; + + InputStream is; + FileOutputStream fos; + BufferedInputStream bis; + BufferedOutputStream bos; + entries = (Enumeration) zip.entries(); + for (int i = 0; entries.hasMoreElements(); i++) { + pauseInterruptHandle(); + + entry = entries.nextElement(); + String path = IO.fitPath(unZipTo) + entry.getName(); + + log(Level.INFO, path); + // 执行解压 + { + File entryFile = IO.file(path); + is = zip.getInputStream(entry); + bis = new BufferedInputStream(is); + fos = new FileOutputStream(entryFile); + bos = new BufferedOutputStream(fos); + + int l; + byte[] buffer = new byte[4096]; + while ((l = bis.read(buffer)) != -1) { + bos.write(buffer, 0, l); + } + } + + bos.flush(); + bos.close(); + fos.close(); + bis.close(); + is.close(); + + progress = 1D * i / total; + } + zip.close(); + + progress = 1; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/task/async/FileZipAsyncTask.java b/src/main/java/com/imyeyu/server/modules/system/task/async/FileZipAsyncTask.java new file mode 100644 index 0000000..f485b73 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/task/async/FileZipAsyncTask.java @@ -0,0 +1,116 @@ +package com.imyeyu.server.modules.system.task.async; + +import ch.qos.logback.classic.Level; +import com.imyeyu.io.IO; +import com.imyeyu.server.modules.system.entity.AsyncTask; +import com.imyeyu.server.modules.system.service.implement.AbstractAsyncTask; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.CRC32; +import java.util.zip.CheckedOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * 压缩 ZIP + * + * @author 夜雨 + * @version 2022-08-15 16:58 + */ +public class FileZipAsyncTask extends AbstractAsyncTask { + + private final String to; + private final List fromList; + + public FileZipAsyncTask(List fromList, String to) { + this.fromList = fromList; + this.to = to; + + type = AsyncTask.Type.FILE_ZIP; + } + + @Override + protected void run() throws Exception { + // 准备文件 Map<文件, 深度路径>,深度路径含文件名 + Map fromList = new HashMap<>(); + File file; + for (int i = 0; i < this.fromList.size(); i++) { + pauseInterruptHandle(); + + file = new File(this.fromList.get(i)); + if (file.exists()) { + if (file.isFile()) { + fromList.put(file, file.getName()); + } else { + String basePath = file.getParentFile().getAbsolutePath(); + List files = IO.listFile(file); + for (int j = 0; j < files.size(); j++) { + fromList.put(files.get(j), files.get(j).getAbsolutePath().substring(basePath.length())); + } + } + } + } + // 压缩文件 + int total = fromList.size(); + int i = 0; + + ZipOutputStream zos = null; + CheckedOutputStream cos = null; + FileOutputStream fos = null; + try { + File zipFile = IO.file(to); + + fos = new FileOutputStream(zipFile); + cos = new CheckedOutputStream(fos, new CRC32()); + zos = new ZipOutputStream(cos); + + for (Map.Entry item : fromList.entrySet()) { + pauseInterruptHandle(); + + log(Level.INFO, item.getKey().getName()); + // 执行压缩 + { + ZipEntry entry = new ZipEntry(item.getValue()); + zos.putNextEntry(entry); + + FileInputStream fis = new FileInputStream(item.getKey()); + BufferedInputStream bis = new BufferedInputStream(fis); + + int l; + byte[] buffer = new byte[4096]; + while ((l = bis.read(buffer)) != -1) { + zos.write(buffer, 0, l); + } + + bis.close(); + fis.close(); + zos.closeEntry(); + } + + // 更新进度 + progress = 1D * ++i / total; + } + progress = 1; + + } finally { + if (zos != null) { + zos.flush(); + zos.close(); + } + if (cos != null) { + cos.flush(); + cos.close(); + } + if (fos != null) { + fos.flush(); + fos.close(); + } + } + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/util/LoggerFilter.java b/src/main/java/com/imyeyu/server/modules/system/util/LoggerFilter.java new file mode 100644 index 0000000..528c629 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/util/LoggerFilter.java @@ -0,0 +1,28 @@ +package com.imyeyu.server.modules.system.util; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.filter.Filter; +import ch.qos.logback.core.spi.FilterReply; + +/** + * 日志过滤 + * + * @author 夜雨 + * @version 2022-07-06 14:54 + */ +public class LoggerFilter extends Filter { + + @Override + public FilterReply decide(ILoggingEvent event) { + // jaudiotagger 越级 + if (event.getLevel() == Level.INFO && event.getLoggerName().startsWith("org.jaudiotagger")) { + return FilterReply.DENY; + } + // oshi COM 异常警告 + if (event.getMessage().contains("COM exception") || event.getMessage().contains("Reflect exception")) { + return FilterReply.DENY; + } + return FilterReply.ACCEPT; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/util/ResourceHandler.java b/src/main/java/com/imyeyu/server/modules/system/util/ResourceHandler.java new file mode 100644 index 0000000..638e500 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/util/ResourceHandler.java @@ -0,0 +1,47 @@ +package com.imyeyu.server.modules.system.util; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.constraints.NotNull; +import com.imyeyu.java.ref.Ref; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.data.mongodb.gridfs.GridFsResource; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; + +import java.nio.file.Path; + +/** + * 文件资源请求处理器 + * + * @author 夜雨 + * @version 2022-07-02 10:14 + */ +@Component +public class ResourceHandler extends ResourceHttpRequestHandler { + + /** + * + * + * @author 夜雨 + * @since 2022-07-02 10:15 + */ + public enum Type { + + FILE, + + MONGO + } + + public static final String ATTR_TYPE = "RESOURCE_HANDLER_TYPE"; + public static final String ATTR_VALUE = "RESOURCE_HANDLER"; + + @Override + protected @NotNull Resource getResource(HttpServletRequest request) { + Type type = Ref.toType(Type.class, request.getAttribute(ATTR_TYPE).toString()); + return switch (type) { + case FILE -> new FileSystemResource((Path) request.getAttribute(ATTR_VALUE)); + case MONGO -> (GridFsResource) request.getAttribute(ATTR_VALUE); + }; + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/util/SystemAPIInterceptor.java b/src/main/java/com/imyeyu/server/modules/system/util/SystemAPIInterceptor.java new file mode 100644 index 0000000..5c3069f --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/util/SystemAPIInterceptor.java @@ -0,0 +1,51 @@ +package com.imyeyu.server.modules.system.util; + +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.spring.TimiSpring; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +/** + * @author 夜雨 + * @version 2023-11-23 17:09 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class SystemAPIInterceptor implements HandlerInterceptor { + + public static final String PATH = "/system/**"; + private static final String IS_SUPER_KEY = "SYSTEM_API_SUPER_KEY"; + + private final SettingService settingService; + + public boolean preHandle(@NonNull HttpServletRequest req, @NonNull HttpServletResponse resp, @NonNull Object handler) { + String key = TimiSpring.getHeader("Key"); + if (TimiJava.isEmpty(key)) { + key = req.getParameter("key"); + } + String dbKey = settingService.getAsString(SettingKey.SYSTEM_API_KEY); + String dbSuperKey = settingService.getAsString(SettingKey.SYSTEM_API_SUPER_KEY); + if (dbSuperKey.equals(key)) { + TimiSpring.setRequestAttr(IS_SUPER_KEY, true); + return true; + } + if (dbKey.equals(key)) { + return true; + } + throw new TimiException(TimiCode.ARG_MISS).msgKey("TODO key invalid"); + } + + public boolean isSuperKey() { + return TimiSpring.hasRequestAttr(IS_SUPER_KEY); + } +} diff --git a/src/main/java/com/imyeyu/server/modules/system/vo/AsyncTaskView.java b/src/main/java/com/imyeyu/server/modules/system/vo/AsyncTaskView.java new file mode 100644 index 0000000..c80fed5 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/vo/AsyncTaskView.java @@ -0,0 +1,16 @@ +package com.imyeyu.server.modules.system.vo; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.imyeyu.server.modules.system.entity.AsyncTask; + +/** + * @author 夜雨 + * @since 2024-12-21 13:29 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class AsyncTaskView extends AsyncTask { + + private long nextExecuteAt; +} diff --git a/src/main/java/com/imyeyu/server/modules/system/vo/ListFileToRequest.java b/src/main/java/com/imyeyu/server/modules/system/vo/ListFileToRequest.java new file mode 100644 index 0000000..5a72a6d --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/vo/ListFileToRequest.java @@ -0,0 +1,21 @@ +package com.imyeyu.server.modules.system.vo; + +import lombok.Data; + +import java.util.List; + +/** + * 文件列表操作对象(复制、剪切、压缩) + * + * @author 夜雨 + * @version 2022-08-13 09:54 + */ +@Data +public class ListFileToRequest { + + /** 文件列表 */ + private List list; + + /** 目标路径 */ + private String to; +} diff --git a/src/main/java/com/imyeyu/server/modules/system/vo/TempAttachRequest.java b/src/main/java/com/imyeyu/server/modules/system/vo/TempAttachRequest.java new file mode 100644 index 0000000..bea0e43 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/vo/TempAttachRequest.java @@ -0,0 +1,19 @@ +package com.imyeyu.server.modules.system.vo; + +import com.imyeyu.server.modules.common.entity.Attachment; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.web.multipart.MultipartFile; + +/** + * TODO 这是个临时对象 + * + * @author 夜雨 + * @version 2024-04-29 00:29 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class TempAttachRequest extends Attachment { + + private MultipartFile[] file; +} diff --git a/src/main/java/com/imyeyu/server/modules/system/vo/terminal/ExecCommand.java b/src/main/java/com/imyeyu/server/modules/system/vo/terminal/ExecCommand.java new file mode 100644 index 0000000..a8c4b22 --- /dev/null +++ b/src/main/java/com/imyeyu/server/modules/system/vo/terminal/ExecCommand.java @@ -0,0 +1,15 @@ +package com.imyeyu.server.modules.system.vo.terminal; + +import lombok.Data; + +/** + * @author 夜雨 + * @version 2024-03-13 12:52 + */ +@Data +public class ExecCommand { + + private String sessionId; + + private String command; +} diff --git a/src/main/java/com/imyeyu/server/util/AES.java b/src/main/java/com/imyeyu/server/util/AES.java new file mode 100644 index 0000000..253afdd --- /dev/null +++ b/src/main/java/com/imyeyu/server/util/AES.java @@ -0,0 +1,84 @@ +package com.imyeyu.server.util; + +import org.springframework.stereotype.Component; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +/** + * AES 加解密 + * + * @author 夜雨 + * @version 2021-08-11 00:52 + */ +@Component +public class AES { + + /** + * 生产密钥 + * + * @return 密钥 + * @throws Exception 生产异常 + */ + public byte[] initKey() throws Exception { + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + return keyGen.generateKey().getEncoded(); + } + + /** + * 加密字符串 + * + * @param data 待加密字符串 + * @param key 密钥 + * @return 加密结果 + * @throws Exception 加密异常 + */ + public byte[] encrypt(String data, byte[] key) throws Exception { + return encrypt(data.getBytes(), key); + } + + /** + * 加密 + * + * @param data 待加密字节数据 + * @param key 密钥 + * @return 加密结果 + * @throws Exception 加密异常 + */ + public byte[] encrypt(byte[] data, byte[] key) throws Exception { + SecretKey secretKey = new SecretKeySpec(key, "AES"); + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); // 密钥 + return cipher.doFinal(data); // 加密返回 + } + + /** + * 解密字符串 + * + * @param data 待解密字节数据 + * @param key 密钥 + * @return 解密结果 + * @throws Exception 解密异常 + */ + public byte[] decrypt(String data, byte[] key) throws Exception { + return decrypt(data.getBytes(), key); + } + + /** + * 解密 + * + * @param data 待解密字节数据 + * @param key 密钥 + * @return 解密结果 + * @throws Exception 解密异常 + */ + public byte[] decrypt(byte[] data, byte[] key) throws Exception { + SecretKey secretKey = new SecretKeySpec(key, "AES"); + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + return cipher.doFinal(data); + } +} diff --git a/src/main/java/com/imyeyu/server/util/CaptchaManager.java b/src/main/java/com/imyeyu/server/util/CaptchaManager.java new file mode 100644 index 0000000..39848af --- /dev/null +++ b/src/main/java/com/imyeyu/server/util/CaptchaManager.java @@ -0,0 +1,189 @@ +package com.imyeyu.server.util; + +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.server.bean.CaptchaFrom; +import com.imyeyu.spring.TimiSpring; +import com.imyeyu.utils.Calc; +import com.imyeyu.utils.Time; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Font; +import java.awt.GradientPaint; +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; +import java.awt.geom.QuadCurve2D; +import java.awt.image.BufferedImage; +import java.security.SecureRandom; +import java.util.Random; + +/** + * 验证码绘制 + * + * @author 夜雨 + * @version 2021-05-20 16:48 + */ +@Slf4j +@Component +public class CaptchaManager { + + /** 会话频率限制时间,毫秒 */ + private static final int LOCK_TIME = 1000; + + /** 会话频率限制键,插值会话 ID */ + private static final String LOCK_KEY = "CAPTCHA:LOCK:%s"; + + /** 会话值缓存键,插值验证码来源 */ + private static final String CACHE_KEY = "CAPTCHA:%s"; + + /** 绘制字符,移除 O,o,I,l */ + private static final char[] CHARS = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' + }; + + @Value("${spring.profiles.active}") + private String env; + + + public BufferedImage generate(CaptchaFrom from, int width, int height) throws InterruptedException { + return generate(from, 4, width, height); + } + + /** + * 生成并缓存验证码 + * + * @param from 来自模块 + * @param width 宽度 + * @param height 高度 + * @return 图片流 + */ + public BufferedImage generate(CaptchaFrom from, int length, int width, int height) throws InterruptedException { + // 加锁 + String lockKey = LOCK_KEY.formatted(TimiSpring.getSession().getId()); + Long lockAt = TimiSpring.getSessionAttr(lockKey, Long.class); + + TimiSpring.setSessionAttr(lockKey, Time.now()); + if (lockAt != null) { + long diff = Time.now() - lockAt; + if (diff < LOCK_TIME) { + // 限制频率 + synchronized (this) { + wait(LOCK_TIME - diff); + } + } + } + + int fontWidth = width / 4; + int fontSize = (int) (height * .8); + int yOffset = (int) (height * .2); + StringBuilder value = new StringBuilder(); + + // 图片流 + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics2D g = (Graphics2D) image.getGraphics(); + // 渐变背景 + GradientPaint gradient = new GradientPaint( + 0, 0, + new Color(Calc.random(200, 255), Calc.random(200, 255), Calc.random(200, 255)), + width, + height, + new Color(Calc.random(200, 255), Calc.random(200, 255), Calc.random(200, 255)) + ); + g.setPaint(gradient); + g.fillRect(0, 0, width, height); + + // 半透明干扰层 + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.4F)); + for (int i = 0; i < 15; i++) { + g.setColor(new Color(Calc.random(0, 150), Calc.random(0, 150), Calc.random(0, 150))); + g.drawString(String.valueOf(CHARS[Calc.random(0, CHARS.length - 1)]), Calc.random(0, width), Calc.random(0, height)); + } + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); + + // 绘制验证码字符 + g.setFont(new Font("Fixedsys", Font.BOLD, Calc.random(fontSize - 2, fontSize + 2))); + for (int i = 0; i < length; i++) { + char c = CHARS[Calc.random(0, CHARS.length - 1)]; + value.append(c); + // 颜色 + g.setColor(new Color(Calc.random(50, 150), Calc.random(50, 150), Calc.random(50, 150))); + // 位置 + int x = fontWidth * i + Calc.random(0, 10); + int y = height - yOffset + Calc.random(-4, 4); + // 随机角度 + AffineTransform original = g.getTransform(); + AffineTransform rotate = AffineTransform.getRotateInstance(Math.toRadians(Calc.random(0, 15)), x + fontSize / 2D, y - fontSize / 2D); + g.setTransform(rotate); + g.drawString(String.valueOf(c), x, y); + g.setTransform(original); + } + // 干扰线 + for (int i = 0; i < height / 2; i++) { + int x1 = Calc.random(0, width); + int y1 = Calc.random(0, height); + int x2 = x1 + Calc.random(-20, 20); + int y2 = y1 + Calc.random(-20, 20); + g.setColor(new Color(Calc.random(100, 150), Calc.random(100, 150), Calc.random(100, 150))); + if (Calc.randomBoolean()) { + g.drawLine(x1, y1, x2, y2); + } else { + QuadCurve2D curve = new QuadCurve2D.Float(); + double ctrlX = (x1 + x2) / 2D + Calc.random(-15, 15); + double ctrlY = (y1 + y2) / 2D + Calc.random(-15, 15); + curve.setCurve(x1, y1, ctrlX, ctrlY, x2, y2); + g.draw(curve); + } + } + // 写入缓存 + TimiSpring.setSessionAttr(CACHE_KEY.formatted(from), value.toString()); + return image; + } + + /** @return 错误回调图像 */ + public BufferedImage error(TimiCode code) { + final int width = 74, height = 24; + BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics2D g = (Graphics2D) img.getGraphics(); + g.setColor(Color.RED); + // 文本 + g.setFont(new Font("Fixedsys", Font.BOLD, 13)); + g.drawString("ERR." + code.getValue(), 2, 18); + g.setColor(Color.DARK_GRAY); + return img; + } + + /** + *

验证。通过验证不抛出任何异常也不返回任何内容,否则抛出相应异常 + *

会验证非空和期限 + *

测试环境总是通过验证 + * + * @param captcha 提交的验证码 + * @param from 来自模块 + * @throws TimiException 验证异常 + */ + public void test(String captcha, String from) { + if (TimiJava.isEmpty(captcha)) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("captcha.miss"); + } + if (env.startsWith("dev")) { + return; + } + // Session 验证 + String sessionCaptcha = TimiSpring.getSessionAttrAsString(CACHE_KEY.formatted(from)); + if (TimiJava.isEmpty(sessionCaptcha)) { + throw new TimiException(TimiCode.ARG_EXPIRED).msgKey("captcha.expire"); + } + if (!captcha.trim().equalsIgnoreCase(sessionCaptcha)) { + throw new TimiException(TimiCode.RESULT_BAD).msgKey("captcha.error"); + } + // 清除缓存 + TimiSpring.removeSessionAttr(CACHE_KEY.formatted(from)); + } +} diff --git a/src/main/java/com/imyeyu/server/util/GsonSerializerAdapter.java b/src/main/java/com/imyeyu/server/util/GsonSerializerAdapter.java new file mode 100644 index 0000000..99f81cd --- /dev/null +++ b/src/main/java/com/imyeyu/server/util/GsonSerializerAdapter.java @@ -0,0 +1,59 @@ +package com.imyeyu.server.util; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.ref.Ref; +import com.imyeyu.server.TimiServerAPI; +import com.imyeyu.server.bean.MultilingualHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Field; +import java.lang.reflect.Type; +import java.util.List; + +/** + * @author 夜雨 + * @version 2023-10-26 10:16 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class GsonSerializerAdapter implements JsonSerializer { + + private final Gson gson; + private final RedisMultilingual redisMultilingual; + + @Override + public JsonElement serialize(Object value, Type typeOfSrc, JsonSerializationContext context) { + if (value instanceof MultilingualHandler _value) { + fillMultilingual(_value); + } + return gson.toJsonTree(value); + } + + private void fillMultilingual(K value) { + try { + List fields = Ref.listFields(value.getClass()); + for (int i = 0; i < fields.size(); i++) { + Field field = fields.get(i); + MultilingualHandler.MultilingualField multiField = field.getAnnotation(MultilingualHandler.MultilingualField.class); + if (multiField != null) { + String multiLangId = Ref.getFieldValue(value, field, String.class); + if (TimiJava.isNotEmpty(multiLangId)) { + Long langId = Long.parseLong(multiLangId); + if (redisMultilingual.map(TimiServerAPI.getUserLanguage()) instanceof RedisLanguage rl) { + Ref.setFieldValue(value, field, rl.textArgs(langId, (Object) multiField.args())); + } + } + } + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/imyeyu/server/util/InitApplication.java b/src/main/java/com/imyeyu/server/util/InitApplication.java new file mode 100644 index 0000000..5cb1309 --- /dev/null +++ b/src/main/java/com/imyeyu/server/util/InitApplication.java @@ -0,0 +1,125 @@ +package com.imyeyu.server.util; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.imyeyu.java.ref.Ref; +import com.imyeyu.lang.mapper.AbstractLanguageMapper; +import com.imyeyu.server.TimiServerAPI; +import com.imyeyu.server.modules.common.bean.SettingKey; +import com.imyeyu.server.modules.common.entity.Multilingual; +import com.imyeyu.server.modules.common.entity.Setting; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.server.modules.system.bean.ServerFile; +import com.imyeyu.spring.util.GlobalReturnHandler; +import com.imyeyu.spring.util.Redis; +import org.eclipse.jgit.api.ArchiveCommand; +import org.eclipse.jgit.archive.TarFormat; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * SpringBoot 启动事件,主要输出基本参数,避免混淆运行环境 + * + * @author 夜雨 + * @version 2021-07-24 14:29 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class InitApplication implements ApplicationRunner { + + @Value("${spring.datasource.timiserver.jdbc-url}") + private String jdbcURL; + + @Value("${spring.redis.host}") + private String redisURL; + + @Value("${spring.redis.port}") + private int redisPort; + + @Value("${spring.profiles.active}") + private String env; + + @Value("${dev.lang}") + private String devLang; + + private final SettingService settingService; + private final RedisMultilingual redisMultilingual; + private final GlobalReturnHandler globalReturnHandler; + private final Redis redisLanguage; + + private void logBaseInfo() { + log.info("JDBC URL: {}", jdbcURL); + log.info("Redis URL: {}:{}", redisURL, redisPort); + log.info("System Setting:"); + List settings = settingService.listAll(); + for (Setting setting : settings) { + String value = Objects.requireNonNullElse(setting.getValue(), ""); + if (64 < value.length()) { + value = value.substring(0, 64) + ".."; + } + value = value.replaceAll("[\\r\\n]+", ""); + log.info("\t{}: {}", setting.getKey(), value); + } + log.info("Init Application Finished."); + } + + private void initGitCommand() { + ArchiveCommand.registerFormat("tar.gz", new TarFormat()); + } + + private void initMultilingual() { +// redisLanguage.flushAll(); + globalReturnHandler.setMultilingualHeader(key -> { + AbstractLanguageMapper map = redisMultilingual.map(TimiServerAPI.getUserLanguage()); + return map.text(key); + }); + } + + private void initFileType() { + JsonObject items = settingService.getAsJsonObject(SettingKey.SYSTEM_FILE_TYPE); + + String[] extensions; + JsonArray extensionsArray; + JsonObject itemObject; + List extensionsList; + for (Map.Entry item : items.entrySet()) { + ServerFile.FileType fileType = Ref.toType(ServerFile.FileType.class, item.getKey()); + itemObject = item.getValue().getAsJsonObject(); + extensionsList = new ArrayList<>(); + extensionsArray = itemObject.get("extensions").getAsJsonArray(); + for (int i = 0; i < extensionsArray.size(); i++) { + if (extensionsArray.get(i).isJsonObject()) { + extensionsList.add(extensionsArray.get(i).getAsJsonObject().get("value").getAsString()); + } else { + extensionsList.add(extensionsArray.get(i).getAsString()); + } + } + extensions = new String[extensionsList.size()]; + // 设置扩展名所属文件类型 + fileType.setExtensions(extensionsList.toArray(extensions)); + } + } + + @Override + public void run(ApplicationArguments args) throws Exception { + Method[] methods = getClass().getDeclaredMethods(); + for (int i = 0; i < methods.length; i++) { + if (!methods[i].getName().equals("run") && !methods[i].getName().contains("$")) { + methods[i].setAccessible(true); + methods[i].invoke(this); + } + } + } +} diff --git a/src/main/java/com/imyeyu/server/util/RedisLanguage.java b/src/main/java/com/imyeyu/server/util/RedisLanguage.java new file mode 100644 index 0000000..53e25d8 --- /dev/null +++ b/src/main/java/com/imyeyu/server/util/RedisLanguage.java @@ -0,0 +1,77 @@ +package com.imyeyu.server.util; + +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.Language; +import com.imyeyu.lang.mapper.AbstractLanguageMapper; +import com.imyeyu.server.TimiServerAPI; +import com.imyeyu.server.modules.common.service.MultilingualService; +import org.jcodec.api.NotSupportedException; +import org.springframework.lang.Nullable; + +import java.util.Arrays; + +/** + * @author 夜雨 + * @version 2024-04-03 11:01 + */ +public class RedisLanguage extends AbstractLanguageMapper { + + public RedisLanguage(Language language) { + super(language); + } + + @Override + public void add(String key, String value) { + throw new NotSupportedException("not supported to add value, MultilingualService will auto cache when not found value"); + } + + @Override + public boolean has(String key) { + return text(key).equals(key); + } + + @Override + public String text(String key) { + String result = TimiServerAPI.applicationContext.getBean(MultilingualService.class).getByKey(language, key); + if (result.startsWith("@")) { + // 递归映射 + return text(result.substring(1)); + } else { + if (result.startsWith("\\@")) { + return result.substring(1); + } else { + return result; + } + } + } + + @Override + public String text(String key, String def) { + String result = text(key); + return result.equals(key) ? def : result; + } + + @Override + public String textArgs(String key, Object... args) { + String result = text(key); + if (result.equals(key)) { + // 没有映射值 + return result + Arrays.toString(args); + } + FORMAT.applyPattern(result); + return FORMAT.format(args); + } + + public String text(Long id) { + return TimiServerAPI.applicationContext.getBean(MultilingualService.class).get(language, id); + } + + public String textArgs(Long id, @Nullable Object... args) { + String result = text(id); + if (TimiJava.isEmpty(args)) { + return result; + } + FORMAT.applyPattern(result); + return FORMAT.format(args); + } +} diff --git a/src/main/java/com/imyeyu/server/util/RedisMultilingual.java b/src/main/java/com/imyeyu/server/util/RedisMultilingual.java new file mode 100644 index 0000000..e44d08f --- /dev/null +++ b/src/main/java/com/imyeyu/server/util/RedisMultilingual.java @@ -0,0 +1,27 @@ +package com.imyeyu.server.util; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import com.imyeyu.java.bean.Language; +import com.imyeyu.lang.multi.Multilingual; +import com.imyeyu.server.modules.common.service.MultilingualService; +import org.springframework.stereotype.Component; + +/** + * @author 夜雨 + * @version 2024-04-03 11:15 + */ +@Component +@RequiredArgsConstructor +public class RedisMultilingual extends Multilingual { + + private final MultilingualService service; + + @PostConstruct + private void postConstruct() { + Language[] languages = Language.values(); + for (int i = 0; i < languages.length; i++) { + add(languages[i], new RedisLanguage(languages[i])); + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..9b03eb6 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,101 @@ +server: + port: 8091 + shutdown: graceful + +# 开发环境语言,激活开发配置时,多语言系统始终使用此语言环境 +dev: + lang: zh_CN + +# 日志配置 +logging: + config: config/logback.xml + +# Spring +spring: + profiles: + active: prod + servlet: + multipart: + max-file-size: 4GB + max-request-size: 4GB + lifecycle: + timeout-per-shutdown-phase: 32s + async: + thread-pool: + core-pool-size: 16 + max-pool-size: 32 + queueCapacity: 16 + keep-alive-seconds: 60 + thread-name-prefix: thread-pool-task-executor- + await-termination-seconds: 60 + mail: # 邮件配置 + host: smtp.qq.com + username: imyeyu@qq.com + password: saodifhaposjfoas + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: true + default-encoding: UTF-8 + mvc: # JSON 序列化 + converters: + preferred-json-mapper: gson # 返回 JSON 序列化使用 GSON + redis: # Redis 数据库 + host: dev.vm.imyeyu.test + port: 6379 + password: + timeout: 8000 + database: + locker: 0 # ID: Integer 全局锁,ID 规范:应用:模块:业务:方法 + multilingual: 1 # ID: Multilingual 多语言缓存 + multilingual-map: 2 # Key: ID 多语言键缓存 + article-ranking: 3 # AID: ArticleRanking(JSON) 热门文章排位 + article-read: 4 # IP: [AID..] IP 阅读文章记录 + user-token: 5 # TOKEN: LONG 用户登录令牌 + user-exp-flag: 6 # UID: NULL 用户登录经验标记,暂时没有值,数据死亡时间为次日零时 + user-email-verify: 7 # AES_KEY: UID 邮箱验证密钥缓存 + user-reset-pw-verify: 8 # AES_KEY: UID 重置密码密钥缓存 + qps-limit: 9 # APP.IP.method: COUNT_IN_LIFE_CYCLE 接口访问频率控制(多个系统公用,需要 App 标记) + setting: 10 # Setting: SettingValue 系统配置 + fmc-player-token: 11 # TOKEN: USER_ID|PLAYER_ID MC 登录缓存 + lettuce: # 连接池配置 + pool: + max-wait: -1 + max-idle: 8 + min-idle: 0 + max-active: 8 + datasource: # 数据库配置 + timiserver: + jdbc-url: jdbc:mysql://dev.vm.imyeyu.test:3306/timi_server?serverTimezone=UTC&characterEncoding=UTF-8 + username: root + password: 123123 + driver-class-name: com.mysql.cj.jdbc.Driver + forevermc: + jdbc-url: jdbc:mysql://dev.vm.imyeyu.test:3306/authme?serverTimezone=UTC&characterEncoding=UTF-8 + username: root + password: 123123 + driver-class-name: com.mysql.cj.jdbc.Driver + gitea: + jdbc-url: jdbc:mysql://dev.vm.imyeyu.test:3306/authme?serverTimezone=UTC&characterEncoding=UTF-8 + username: root + password: 123123 + driver-class-name: com.mysql.cj.jdbc.Driver + data: + mongodb: + host: dev.vm.imyeyu.test + port: 27017 + database: db + username: root + password: qweqwe123 + +# CORS 跨域 +cors: + # 允许访问的客户端域名,如:http://web.xxx.com,* 表示不做任何限制(不做任何限制时 allow-credentials 无效) + allow-origin: + - "http://localhost:8080" + allow-methods: "*" # 允许请求的方法名,多个方法名逗号分割,如:GET, POST, PUT, DELETE, OPTIONS + allow-credentials: true # 是否允许请求带有验证信息,若要获取客户端域下的 Cookie 或 Session 时,设置为 true + allow-headers: "*" # 允许服务端访问的客户端请求头,多个请求头逗号分割,如:Content-BizType diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000..0cefe81 --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,9 @@ + ______ _ __ _ _ + / __\ (_)_ __ ___ _ _ ___ _ _ _ _ ___ ___ _ __ ___ \ \ \ \ + / . . \ ' | | '_ _ \| | | |/ _ \ | | | | | | / __/ _ \| '_ _ \ \ \ \ \ +( ) | | | | | | | |_| | __/ |_| | |_| || (_| (_) | | | | | | ) ) ) ) +'\ ___ /' |_|_| |_| |_|\__, |\___|\__, |\__,_(_)___\___/|_| |_| |_| / / / / +--'---'-------------------|___/------|___/-----------------------------/_/_/_/ + +Banner drawn by https://patorjk.com/software/taag for Ivrit +Spring Boot Version: ${spring-boot.version} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..c0e174a --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,25 @@ + + + + UTF-8 + [%d{HH:mm:ss.SSS}][%-5level][%-54logger{54}] %msg%n + + + + + ./logs/debug.log + + UTF-8 + [%d{HH:mm:ss.SSS}][%-5level][%-54logger{54}] %msg%n + + + ./logs/debug/debug.%d{yyyy-MM-dd}.log + 30 + + + + + + + + diff --git a/src/main/resources/mapper/blog/ArticleMapper.xml b/src/main/resources/mapper/blog/ArticleMapper.xml new file mode 100644 index 0000000..839eb71 --- /dev/null +++ b/src/main/resources/mapper/blog/ArticleMapper.xml @@ -0,0 +1,65 @@ + + + + article + + + + article.id, + article.type, + article.title, + article.digest, + article.likes, + article.reads, + article.created_at, + article.updated_at + + + + + + FROM + article + WHERE + 1 < article.id + AND article.deleted_at IS NULL + + + + + + + + FROM + article + WHERE + 1 < article.id + AND ( + article.title LIKE CONCAT('%', #{keyword}, '%') + OR article.digest LIKE CONCAT('%', #{keyword}, '%') + ) + AND article.deleted_at IS NULL + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/common/AttachmentMapper.xml b/src/main/resources/mapper/common/AttachmentMapper.xml new file mode 100644 index 0000000..50c9af5 --- /dev/null +++ b/src/main/resources/mapper/common/AttachmentMapper.xml @@ -0,0 +1,22 @@ + + + + attachment + + \ No newline at end of file diff --git a/src/main/resources/mapper/common/CommentMapper.xml b/src/main/resources/mapper/common/CommentMapper.xml new file mode 100644 index 0000000..5aaedcf --- /dev/null +++ b/src/main/resources/mapper/common/CommentMapper.xml @@ -0,0 +1,143 @@ + + + + comment + + + + + + + + + + + + + + + + + + + + + + + + + FROM + + WHERE + user_id = #{userId} + AND deleted_at IS NULL + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/common/IconMapper.xml b/src/main/resources/mapper/common/IconMapper.xml new file mode 100644 index 0000000..f1afd30 --- /dev/null +++ b/src/main/resources/mapper/common/IconMapper.xml @@ -0,0 +1,35 @@ + + + + icon + + + FROM ( + SELECT + id + FROM + icon_labels + WHERE + ${lang} LIKE CONCAT('%', #{label}, '%') + AND icon_labels.deleted_at IS NULL + ) AS labels + LEFT JOIN + icon_label + ON + icon_label.icon_label_id = labels.id + AND labels.id IS NOT NULL + LEFT JOIN + + ON + icon.id = icon_label.icon_id + WHERE + icon.id IS NOT NULL + AND icon.deleted_at IS NULL + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/common/MultilingualMapper.xml b/src/main/resources/mapper/common/MultilingualMapper.xml new file mode 100644 index 0000000..2eb6989 --- /dev/null +++ b/src/main/resources/mapper/common/MultilingualMapper.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/common/SettingMapper.xml b/src/main/resources/mapper/common/SettingMapper.xml new file mode 100644 index 0000000..80e1e63 --- /dev/null +++ b/src/main/resources/mapper/common/SettingMapper.xml @@ -0,0 +1,16 @@ + + + + setting + + \ No newline at end of file diff --git a/src/main/resources/mapper/common/TaskMapper.xml b/src/main/resources/mapper/common/TaskMapper.xml new file mode 100644 index 0000000..7364521 --- /dev/null +++ b/src/main/resources/mapper/common/TaskMapper.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/common/UserConfigMapper.xml b/src/main/resources/mapper/common/UserConfigMapper.xml new file mode 100644 index 0000000..558780d --- /dev/null +++ b/src/main/resources/mapper/common/UserConfigMapper.xml @@ -0,0 +1,5 @@ + + + + user_config + \ No newline at end of file diff --git a/src/main/resources/mapper/common/UserPrivacyMapper.xml b/src/main/resources/mapper/common/UserPrivacyMapper.xml new file mode 100644 index 0000000..3d7ea1f --- /dev/null +++ b/src/main/resources/mapper/common/UserPrivacyMapper.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/common/UserProfileMapper.xml b/src/main/resources/mapper/common/UserProfileMapper.xml new file mode 100644 index 0000000..2aad3d2 --- /dev/null +++ b/src/main/resources/mapper/common/UserProfileMapper.xml @@ -0,0 +1,5 @@ + + + + user_profile + \ No newline at end of file diff --git a/src/main/resources/mapper/git/IssueMapper.xml b/src/main/resources/mapper/git/IssueMapper.xml new file mode 100644 index 0000000..dd79400 --- /dev/null +++ b/src/main/resources/mapper/git/IssueMapper.xml @@ -0,0 +1,40 @@ + + + + git_issue + + repository_id = #{issuePage.repositoryId} + + AND type = #{issuePage.type} + + + AND status = #{issuePage.status} + + + AND ( + title LIKE CONCAT('%', #{issuePage.keyword}, '%') + OR description LIKE CONCAT('%', #{issuePage.keyword}, '%') + ) + + AND deleted_at IS NULL + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/git/MergeMapper.xml b/src/main/resources/mapper/git/MergeMapper.xml new file mode 100644 index 0000000..103f55b --- /dev/null +++ b/src/main/resources/mapper/git/MergeMapper.xml @@ -0,0 +1,40 @@ + + + + git_merge + + repository_id = #{mergePage.repositoryId} + + AND type = #{mergePage.type} + + + AND status = #{mergePage.status} + + + AND ( + title LIKE CONCAT('%', #{mergePage.keyword}, '%') + OR description LIKE CONCAT('%', #{mergePage.keyword}, '%') + ) + + AND deleted_at IS NULL + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/git/ReleaseMapper.xml b/src/main/resources/mapper/git/ReleaseMapper.xml new file mode 100644 index 0000000..6925f79 --- /dev/null +++ b/src/main/resources/mapper/git/ReleaseMapper.xml @@ -0,0 +1,43 @@ + + + + git_release + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/gitea/ActionMapper.xml b/src/main/resources/mapper/gitea/ActionMapper.xml new file mode 100644 index 0000000..a4895bb --- /dev/null +++ b/src/main/resources/mapper/gitea/ActionMapper.xml @@ -0,0 +1,52 @@ + + + + + + diff --git a/src/main/resources/mapper/minecraft/PackMapper.xml b/src/main/resources/mapper/minecraft/PackMapper.xml new file mode 100644 index 0000000..952f82d --- /dev/null +++ b/src/main/resources/mapper/minecraft/PackMapper.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/minecraft/PlayerMapper.xml b/src/main/resources/mapper/minecraft/PlayerMapper.xml new file mode 100644 index 0000000..9e21d04 --- /dev/null +++ b/src/main/resources/mapper/minecraft/PlayerMapper.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/mapper/system/AsyncTaskMapper.xml b/src/main/resources/mapper/system/AsyncTaskMapper.xml new file mode 100644 index 0000000..e99da35 --- /dev/null +++ b/src/main/resources/mapper/system/AsyncTaskMapper.xml @@ -0,0 +1,56 @@ + + + + async_task + + INSERT INTO ( + uuid, + name, + type, + message, + status, + progress, + can_pause, + can_interrupt, + is_periodical, + cron, + start_at, + interrupt_at, + error_at, + died_at, + created_at + ) VALUES ( + #{uuid}, + #{name}, + #{type}, + #{message}, + #{status}, + #{progress}, + #{canPause}, + #{canInterrupt}, + #{isPeriodical}, + #{cron}, + #{startAt}, + #{interruptAt}, + #{errorAt}, + #{diedAt}, + #{createdAt} + ) + + + UPDATE + + SET + name = #{name}, + type = #{type}, + message = #{message}, + status = #{status}, + progress = #{progress}, + start_at = #{startAt}, + interrupt_at = #{interruptAt}, + error_at = #{errorAt}, + died_at = #{diedAt} + WHERE + uuid = #{uuid} + + diff --git a/src/main/resources/templates/EmailVerify.ftl b/src/main/resources/templates/EmailVerify.ftl new file mode 100644 index 0000000..e9014e9 --- /dev/null +++ b/src/main/resources/templates/EmailVerify.ftl @@ -0,0 +1,58 @@ + + + + + Title + + + + +
+
+ <#if user.data.hasWrapper> + + <#else> + + +

+ ${user.name} + 您好,您正在验证 + 夜雨博客 + 的邮箱绑定,请在 10 分钟内点击下面的按钮完成校验,让我确定这是你的电子邮箱,不要泄露此邮件内容。 +

+ 继续完成验证 + + <#include "Footer.ftl" /> +
+
+
+
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/Footer.ftl b/src/main/resources/templates/Footer.ftl new file mode 100644 index 0000000..49b9b77 --- /dev/null +++ b/src/main/resources/templates/Footer.ftl @@ -0,0 +1,11 @@ +
+

这是由系统自动发送的邮件,请不要回复

+

+ 如果这封邮件打扰到您,请直接屏蔽本邮箱或登录 + 夜雨博客 + 个人空间中的设置随时关闭 +

+

 

+

朝朝频顾惜,夜夜不相忘

+

Copyright © 2017 - ${.now?string["yyyy"]} 夜雨 All Rights Reserved 版权所有

+
\ No newline at end of file diff --git a/src/main/resources/templates/ReplyRemind.ftl b/src/main/resources/templates/ReplyRemind.ftl new file mode 100644 index 0000000..abd8215 --- /dev/null +++ b/src/main/resources/templates/ReplyRemind.ftl @@ -0,0 +1,92 @@ + + + + + Title + + + + +
+
+ <#if user.data.hasWrapper> + + <#else> + + +

+ ${user.name} + 您好,您在 + 夜雨博客 + 的评论收到以下回复 + (24 小时内) +

+ <#list reminds as remind> +
+
+ <#if remind.reply.sender??> + <#if remind.reply.sender.data.avatarType> + + <#else> + + +

${remind.reply.sender.name} 说:

+ <#else> + +

${remind.reply.senderNick} 说:

+ +

${remind.reply.data}

+
+ ${remind.reply.createdAt?number_to_datetime?string("yyyy-MM-dd HH:mm:ss")} +
+ + + <#include "Footer.ftl" /> +
+
+
+
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/ResetPassword.ftl b/src/main/resources/templates/ResetPassword.ftl new file mode 100644 index 0000000..b61aa8e --- /dev/null +++ b/src/main/resources/templates/ResetPassword.ftl @@ -0,0 +1,63 @@ + + + + + Title + + + + +
+
+ <#if user.data.hasWrapper> + user wrapper + <#else> + user wrapper + +

+ ${user.name} + 您好, + 夜雨博客 + 收到了您的重置密码请求,请在 10 分钟内点击下面的按钮继续完成,不要泄露此邮件内容。 +

+ 重置密码 + + <#include "Footer.ftl" /> +
+
+
+
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/StyleSheet.ftl b/src/main/resources/templates/StyleSheet.ftl new file mode 100644 index 0000000..50faf6c --- /dev/null +++ b/src/main/resources/templates/StyleSheet.ftl @@ -0,0 +1,163 @@ +/* 基本样式 */ +.font12 { + font-size: 12px; +} + +.gray { + color: #999; +} + +.pink { + color: #FF7A9B; +} + +.bold { + font-weight: bold; +} + +.center { + text-align: center; +} + +a { + cursor: pointer !important; +} + +p { + margin: 0; +} + +.clip-text { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.ir-pixelated { + image-rendering: pixelated; +} + +.ir-auto { + image-rendering: auto; +} + +.ir-smooth { + image-rendering: smooth; +} + +/* 布局样式 */ + +.root { + width: 42rem; + margin: 24px auto; + position: relative; +} + +.mail { + margin: 0 3rem; + box-shadow: 0 0 8px rgba(0, 0, 0, .5); + background: #F4F4F4; +} + +.wrapper { + width: 100%; + display: block; + border-bottom: 1px solid #CDDEF0; +} + +.title { + margin: 4px 12px; +} + +.footer { + color: #555; + padding: 12px 0; + margin-top: 32px; +} + +.line1 { + height: 3px; + z-index: -1; + position: relative; + margin-top: -50px; + background: #FF7A9B; +} + +.line1::before { + content: ""; + left: 0; + width: 50%; + height: 3px; + bottom: 0; + position: absolute; + background: #FF7A9B; + transform: rotate(-30deg); + transform-origin: 0 0; +} + +.line1::after { + content: ""; + right: 0; + width: 50%; + height: 3px; + bottom: 0; + position: absolute; + background: #FF7A9B; + transform: rotate(30deg); + transform-origin: 100% 0; +} + +.line2, +.line3 { + width: 3px; + height: 200px; + position: absolute; + background: linear-gradient(to bottom, rgba(255,122,155,1) 0%, rgba(255,255,255,0) 100%);; +} + +.line2::before, +.line3::before { + content: ""; + width: 200px; + height: 40px; + position: absolute; + background: #FFF; +} + +.line2::before { + top: 33px; + left: 12px; + transform: rotate(16deg); +} + +.line3::before { + top: 32px; + right: 12px; + transform: rotate(-16deg); +} + +.line2::after, +.line3::after { + content: ""; + top: -1px; + width: 21.8rem; + height: 3px; + position: absolute; + background: #FF7A9B; +} + +.line2::after { + left: 2px; + transform: rotate(16deg); + transform-origin: 0 0; +} + +.line3::after { + right: 2px; + transform: rotate(-16deg); + transform-origin: 100% 0; +} + +.line3 { + right: 0; +} \ No newline at end of file diff --git a/src/test/java/test/SpringLang.java b/src/test/java/test/SpringLang.java new file mode 100644 index 0000000..4ad5482 --- /dev/null +++ b/src/test/java/test/SpringLang.java @@ -0,0 +1,371 @@ +package test; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.imyeyu.io.IO; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.CallbackArgReturn; +import com.imyeyu.java.bean.Language; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.java.ref.Ref; +import com.imyeyu.network.FormMap; +import com.imyeyu.server.TimiServerAPI; +import com.imyeyu.server.modules.common.entity.Multilingual; +import com.imyeyu.server.modules.common.mapper.MultilingualMapper; +import com.imyeyu.utils.Digest; +import com.imyeyu.utils.Encoder; +import com.imyeyu.utils.Time; +import lombok.AllArgsConstructor; +import org.apache.hc.client5.http.fluent.Request; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@SpringBootTest(classes = TimiServerAPI.class) +@RunWith(SpringRunner.class) +public class SpringLang { + + @Autowired + private MultilingualMapper mapper; + + private static final String API = "http://api.fanyi.baidu.com/api/trans/vip/translate?"; + + @Test + public void temp() throws Exception { + List multilinguals = mapper.selectByKeyLike(".exception."); + for (int i = 0; i < multilinguals.size(); i++) { + Multilingual item = multilinguals.get(i); + item.setKey(item.getKey().replaceAll("exception", "tips")); + mapper.update(item); + } + } + + @Test + public void addLang() throws Exception { + Map map = new HashMap<>(); + + for (Map.Entry item : map.entrySet()) { + Multilingual exist = mapper.selectByKey(item.getKey()); + if (exist != null) { + System.err.println("exist key: " + item.getKey()); + continue; + } + Multilingual multilingual = new Multilingual(); + multilingual.setKey(item.getKey()); + multilingual.setZhCN(item.getValue()); + multilingual.setCreatedAt(Time.now()); + mapper.insert(multilingual); + } + } + + @Test + public void addLang4File() throws Exception { + Properties properties = new Properties(); + properties.load(IO.getInputStream(new File("newLang.properties"))); + + for (Map.Entry item : properties.entrySet()) { + Multilingual exist = mapper.selectByKey(item.getKey().toString()); + if (exist != null) { + System.err.println("exist key: " + item.getKey()); + continue; + } + Multilingual multilingual = new Multilingual(); + multilingual.setKey(item.getKey().toString()); + multilingual.setZhCN(item.getValue().toString()); + multilingual.setCreatedAt(Time.now()); + mapper.insert(multilingual); + } + } + + @Test + public void appendTranslateResult4Path() throws Exception { + String path = ""; + // DB_KEY, FILE_KEY + Map keyMap = new HashMap<>(); + keyMap.put("", ""); + + for (Map.Entry item : keyMap.entrySet()) { + Language[] languages = Language.values(); + for (int i = 0; i < languages.length; i++) { + if (languages[i] == Language.zh_CN) { + continue; + } + Properties properties = new Properties(); + File file = new File(path + "%s.lang".formatted(languages[i].toString())); + if (!file.exists()) { + System.err.println(languages[i] + " not exist"); + continue; + } + properties.load(IO.getInputStream(file)); + + Multilingual multilingual = mapper.selectByKey(item.getKey()); + String fieldName = languages[i].toString().replace("_", ""); + Ref.setFieldValue(multilingual, fieldName, properties.getProperty(item.getValue())); + mapper.update(multilingual); + } + } + } + + @Test + public void upper() { + List list = mapper.list(0, 9999); + for (int i = 0; i < list.size(); i++) { + Multilingual multilingual = list.get(i); + multilingual.setEnUS(String.valueOf(multilingual.getEnUS().charAt(0)).toUpperCase() + multilingual.getEnUS().substring(1)); + mapper.update(multilingual); + } + } + + @Test + public void translate() throws Exception { + List data = mapper.selectByNotTranslate(); + Map cnMap = new HashMap<>(); + for (int i = 0; i < data.size(); i++) { + StringBuilder sb = new StringBuilder(); + for (int j = 0; j < Math.min(data.size() - i, 20); j++, i++) { + Multilingual multilingual = data.get(i); + sb.append(multilingual.getZhCN()).append("\r\n"); + cnMap.put(multilingual.getZhCN(), multilingual); + } + i--; + System.out.println("translate " + sb); + List languageList = BaiduLanguage.valuesWithout(BaiduLanguage.ZH); + for (int j = 0; j < languageList.size(); j++) { + Map result = doTranslate(languageList.get(j), sb.toString()); + for (Map.Entry item : result.entrySet()) { + Multilingual multilingual = cnMap.get(item.getKey()); + Language lang = languageList.get(j).language; + String value = multilingual.getValue(lang); + if (TimiJava.isEmpty(value)) { + Ref.setFieldValue(multilingual, lang.toString().replace("_", ""), item.getValue()); + } + mapper.update(multilingual); + } + } + Thread.sleep(1000); + } + } + +// 数据库查重 +// SELECT * FROM multilingual WHERE zh_cn IN (SELECT zh_cn FROM multilingual GROUP BY zh_cn HAVING COUNT(zh_cn) > 1); + +// SELECT * FROM multilingual WHERE key IN (SELECT key FROM multilingual GROUP BY key HAVING COUNT(key) > 1); + + @Test + public void export() throws Exception { + Pattern compile = Pattern.compile("\\.(text|textArgs)\\(\"(.*?)\""); + + File scanDir = new File("E:\\IDEAProject\\ForeverMC\\src"); + File outDir = new File("E:\\IDEAProject\\ForeverMC\\src\\main\\resources\\lang"); + + // 排除键 +// String[] excludePath = {}; + String[] excludePath = {"E:\\IDEAProject\\timi-fx-ui\\src\\main\\resources\\lang\\timi-fx-ui"}; + Set excludeKeys = new HashSet<>(); + if (excludePath.length != 0) { + for (int i = 0; i < excludePath.length; i++) { + InputStream is = IO.getInputStream(new File(excludePath[i] + "\\zh_CN.lang")); + Properties properties = new Properties(); + properties.load(is); + Map map = new HashMap<>(properties); + excludeKeys.addAll(map.keySet().stream().map(Object::toString).collect(Collectors.toSet())); + } + } + + // 扫描文件获取键 + List files = IO.listFile(scanDir); + Set keySet = new HashSet<>(); + for (int i = 0; i < files.size(); i++) { + File file = files.get(i); + if (file.isDirectory()) { + continue; + } + if (!file.getName().endsWith(".java")) { + continue; + } + String fileData = IO.toString(file); + + Matcher matcher = compile.matcher(fileData); + while (matcher.find()) { + String key = matcher.group(2); + if (!excludeKeys.contains(key)) { + keySet.add(key); + } + } + } + + StringBuilder notFoundKey = new StringBuilder(); + + List keyList = new ArrayList<>(keySet); + keyList.sort(Comparator.naturalOrder()); + List result = mapper.selectByKeyList(keyList); + { + List deepQueryKeyList; + CallbackArgReturn, List> newKeyList = mList -> { + List arr = new ArrayList<>(); + for (int i = 0; i < mList.size(); i++) { + if (mList.get(i).getZhCN().startsWith("@")) { + String newKey = mList.get(i).getZhCN().substring(1); + if (!keyList.contains(newKey)) { + arr.add(newKey); + } + } + } + return arr; + }; + deepQueryKeyList = newKeyList.handler(result); + do { + List deepResult = new ArrayList<>(); + for (int i = 0; i < deepQueryKeyList.size(); i++) { + Multilingual r = mapper.selectByKey(deepQueryKeyList.get(i)); + if (r == null) { + notFoundKey.append("[deep]").append(deepQueryKeyList.get(i)).append('\n'); + } else { + deepResult.add(r); + } + } + result.addAll(deepResult); + deepQueryKeyList = newKeyList.handler(deepResult); + } while (!deepQueryKeyList.isEmpty()); + } + + result.sort(Comparator.comparing(Multilingual::getKey)); + + Set resultKey = result.stream().map(Multilingual::getKey).collect(Collectors.toSet()); + for (String key : keyList) { + if (!resultKey.contains(key)) { + notFoundKey.append(key).append('\n'); + } + } + if (!notFoundKey.isEmpty()) { + System.err.printf("not found key in db: %n------%n%s------%n", notFoundKey); + } else { + // 输出文件 + Language[] languages = Language.values(); + for (int i = 0; i < languages.length; i++) { + String fieldName = languages[i].toString().replace("_", ""); + + File file = IO.file(IO.fitPath(outDir.getAbsolutePath()) + languages[i] + ".lang"); + StringBuilder sb = new StringBuilder(); + for (int j = 0; j < result.size(); j++) { + String value = Ref.getFieldValue(result.get(j), fieldName, String.class).trim(); + value = value.replaceAll("\n", "\\\\n\\\\\n"); + sb.append(result.get(j).getKey()).append("=").append(value); + sb.append('\n'); + } + IO.toFile(file, sb.toString()); + } + System.out.println("write successful"); + } + } + + /** + * 文本翻译 + * + * @param text 原文本 + * @param to 目标语言 + * @return Map<原数据,翻译结果> + * @throws Exception 翻译异常 + */ + private synchronized Map doTranslate(BaiduLanguage to, String text) throws Exception { + String random = String.valueOf(System.currentTimeMillis()); + + String appId = "20180920000210118"; + String key = "MfI4Iu0go3541Ryx3f6K"; + + FormMap args = new FormMap<>(); + args.put("q", text); + args.put("from", "ZH".toLowerCase()); + args.put("to", to.toString().toLowerCase()); + args.put("appid", appId); + args.put("salt", random); + args.put("sign", Digest.md5(appId + text + random + key)); + + String response = Request.post(API + Encoder.urlArgs(args)).bodyForm(args.build()).execute().returnContent().asString(); + JsonObject jo = JsonParser.parseString(response).getAsJsonObject(); + if (jo.has("error_code")) { + System.err.println(jo); + throw new TimiException(TimiCode.ERROR, jo.get("error_msg").getAsString()); + } + JsonArray ja = jo.get("trans_result").getAsJsonArray(); + + JsonObject resultJO; + Map result = new HashMap<>(); + for (int i = 0; i < ja.size(); i++) { + resultJO = ja.get(i).getAsJsonObject(); + result.put(resultJO.get("src").getAsString(), resultJO.get("dst").getAsString()); + } + + wait(200); + return result; + } + + @AllArgsConstructor + public enum BaiduLanguage { + + ZH(Language.zh_CN), + EN(Language.en_US), + JP(Language.ja_JP), + KOR(Language.ko_KR), + RU(Language.ru_RU), + DE(Language.de_DE), + CHT(Language.zh_TW); + + /** 标准映射 */ + final Language language; + + + /** + * 根据映射查找 + * + * @param language 映射 + * @return 枚举对象 + */ + static BaiduLanguage fromMapping(Language language) { + for (BaiduLanguage type : BaiduLanguage.values()) { + if (type.language.toString().equalsIgnoreCase(language.toString())) { + return type; + } + } + return null; + } + + + /** + * 获取排除语言列表 + * + * @param baiduLanguage 排除语言 + * @return 语言列表 + */ + static List valuesWithout(BaiduLanguage... baiduLanguage) { + Set outList = Set.of(baiduLanguage); + + List result = new ArrayList<>(); + BaiduLanguage[] values = values(); + for (int i = 0; i < values.length; i++) { + if (!outList.contains(values[i])) { + result.add(values[i]); + } + } + return result; + } + } +} diff --git a/src/test/java/test/SpringTest.java b/src/test/java/test/SpringTest.java new file mode 100644 index 0000000..56380cc --- /dev/null +++ b/src/test/java/test/SpringTest.java @@ -0,0 +1,113 @@ +package test; + +import com.google.gson.Gson; +import com.imyeyu.server.TimiServerAPI; +import com.imyeyu.server.modules.blog.entity.Article; +import com.imyeyu.server.modules.blog.mapper.ArticleMapper; +import com.imyeyu.server.modules.common.entity.Icon; +import com.imyeyu.server.modules.common.mapper.AttachmentMapper; +import com.imyeyu.server.modules.common.mapper.IconMapper; +import com.imyeyu.server.modules.common.mapper.MultilingualMapper; +import com.imyeyu.server.modules.common.service.SettingService; +import com.imyeyu.server.modules.forevermc.entity.Server; +import com.imyeyu.server.modules.forevermc.mapper.ServerMapper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@SpringBootTest(classes = TimiServerAPI.class) +@RunWith(SpringRunner.class) +public class SpringTest { + + @Autowired + private IconMapper iconMapper; + + @Autowired + private MultilingualMapper multilingualMapper; + + @Autowired + private SettingService settingService; + + @Autowired + private ServerMapper serverMapper; + + @Autowired + private ArticleMapper articleMapper; + + @Autowired + private AttachmentMapper attachmentMapper; + + @Test + public void testBaseMapper() { + String id; + Server select; + { + // 插入 + Server server = new Server(); + server.setTitle("test insert title"); + server.setDescription("test insert desc"); + server.setHost("test insert host"); + serverMapper.insert(server); + id = server.getId(); + + // 查询 + select = serverMapper.select(id); + assert select.getId().equals(id); + assert select.getTitle().equals(server.getTitle()); + assert select.getCreatedAt() != null; + } + { + // 更新 + select = serverMapper.select(id); + select.setTitle("test update title"); + serverMapper.update(select); + + select = serverMapper.select(id); + assert select.getId().equals(id); + assert select.getTitle().equals("test update title"); + assert select.getUpdatedAt() != null; + } + { + // 软删 + serverMapper.delete(id); + + select = serverMapper.select(id); + assert select == null; + } + { + // 硬删 + System.out.println("destroy id = " + id); + serverMapper.destroy(id); + } + } + + + @Test + public void testBaseMapperGenId() { + Article last = articleMapper.select(128L); + + Article test = new Article(); + test.setTitle("title"); + test.setData("data"); + articleMapper.insert(test); + assert test.getId() != null && test.getId() - 1 == last.getId(); + + articleMapper.destroy(test.getId()); + } + + @Test + public void iconJson() throws Exception { + List icon = iconMapper.listAll(); + Map map = new HashMap<>(); + for (int i = 0; i < icon.size(); i++) { + map.put(icon.get(i).getName(), icon.get(i).getSvg()); + } + System.out.println(new Gson().toJson(map)); + } +} diff --git a/src/test/java/test/Test.java b/src/test/java/test/Test.java new file mode 100644 index 0000000..b820cc4 --- /dev/null +++ b/src/test/java/test/Test.java @@ -0,0 +1,27 @@ +package test; + +import com.imyeyu.io.IOSize; +import com.imyeyu.utils.Text; +import com.imyeyu.utils.Time; + +import java.util.UUID; + +/** + * @author 夜雨 + * @version 2023-11-10 16:34 + */ +public class Test { + + public static void main(String[] args) throws Exception { + + UUID uuid = UUID.randomUUID(); + System.out.println(Time.now()); + System.out.println(uuid); + System.out.println(uuid.toString().toUpperCase()); + System.out.println(Text.randomString(6)); + System.out.println(Text.randomString(8)); + System.out.println(Text.randomString(16)); + System.out.println(Text.randomString(32)); + System.out.println(Text.randomString(64)); + } +}