├── src ├── main │ ├── resources │ │ ├── static │ │ │ ├── fonts │ │ │ │ └── iconmoon │ │ │ │ │ ├── icomoon.eot │ │ │ │ │ ├── icomoon.ttf │ │ │ │ │ ├── icomoon.woff │ │ │ │ │ └── icomoon.svg │ │ │ ├── favicon.ico │ │ │ ├── img │ │ │ │ ├── pin.png │ │ │ │ ├── avatar.jpg │ │ │ │ ├── err_img.jpg │ │ │ │ ├── loading.gif │ │ │ │ ├── about_banner.png │ │ │ │ ├── home_banner.jpg │ │ │ │ ├── archive_banner.png │ │ │ │ ├── scrollbar-img.jpg │ │ │ │ └── friend_link_banner.png │ │ │ ├── css │ │ │ │ ├── admin │ │ │ │ │ ├── index.min.css │ │ │ │ │ ├── file.min.css │ │ │ │ │ ├── mail.min.css │ │ │ │ │ ├── role.min.css │ │ │ │ │ ├── user.min.css │ │ │ │ │ ├── index.css │ │ │ │ │ ├── login.min.css │ │ │ │ │ ├── friend_link.min.css │ │ │ │ │ ├── permission.min.css │ │ │ │ │ ├── system_config.min.css │ │ │ │ │ ├── file.css │ │ │ │ │ ├── mail.css │ │ │ │ │ ├── role.css │ │ │ │ │ ├── user.css │ │ │ │ │ ├── login.css │ │ │ │ │ ├── permission.css │ │ │ │ │ ├── friend_link.css │ │ │ │ │ ├── system_config.css │ │ │ │ │ ├── comment.min.css │ │ │ │ │ ├── article.min.css │ │ │ │ │ ├── comment.css │ │ │ │ │ └── article.css │ │ │ │ ├── about.min.css │ │ │ │ ├── article_list.min.css │ │ │ │ ├── about.css │ │ │ │ ├── footer.min.css │ │ │ │ ├── header.min.css │ │ │ │ ├── index.min.css │ │ │ │ ├── article_list.css │ │ │ │ ├── footer.css │ │ │ │ ├── article.min.css │ │ │ │ ├── index.css │ │ │ │ ├── header.css │ │ │ │ ├── article.css │ │ │ │ ├── iconmoon.min.css │ │ │ │ ├── about_footer.min.css │ │ │ │ ├── about_footer.css │ │ │ │ ├── iconmoon.css │ │ │ │ ├── friend_link_list.min.css │ │ │ │ ├── toc.min.css │ │ │ │ └── error.min.css │ │ │ ├── js │ │ │ │ ├── constants.js │ │ │ │ ├── admin │ │ │ │ │ └── constants.js │ │ │ │ └── utils.js │ │ │ └── plugin │ │ │ │ ├── dompurify │ │ │ │ └── dompurify.config.js │ │ │ │ ├── marked │ │ │ │ └── marked.renderer.js │ │ │ │ ├── highlight │ │ │ │ └── custom-style.css │ │ │ │ └── message │ │ │ │ └── message.min.js │ │ ├── sql │ │ │ ├── 07_init_user_role.sql │ │ │ ├── 03_init_role.sql │ │ │ ├── 06_init_user.sql │ │ │ ├── 09_20230106_增加开往社区链接配置.sql │ │ │ └── 08_20221101_增加失败邮件重发功能.sql │ │ ├── mapper │ │ │ ├── FileMapper.xml │ │ │ ├── MailMapper.xml │ │ │ ├── RoleMapper.xml │ │ │ ├── UserMapper.xml │ │ │ ├── ArticleMapper.xml │ │ │ ├── CommentMapper.xml │ │ │ ├── ContentMapper.xml │ │ │ ├── UserRoleMapper.xml │ │ │ ├── FriendLinkMapper.xml │ │ │ ├── PermissionMapper.xml │ │ │ ├── SystemConfigMapper.xml │ │ │ └── RolePermissionMapper.xml │ │ ├── templates │ │ │ ├── component │ │ │ │ ├── admin │ │ │ │ │ ├── file │ │ │ │ │ │ └── file_module.html │ │ │ │ │ ├── mail │ │ │ │ │ │ └── mail_module.html │ │ │ │ │ ├── comment │ │ │ │ │ │ └── comment_module.html │ │ │ │ │ ├── article │ │ │ │ │ │ ├── preview_article_panel.html │ │ │ │ │ │ └── article_module.html │ │ │ │ │ ├── system_config │ │ │ │ │ │ ├── system_config_module.html │ │ │ │ │ │ └── edit_system_config_panel.html │ │ │ │ │ ├── permission │ │ │ │ │ │ ├── permission_module.html │ │ │ │ │ │ ├── add_permission_panel.html │ │ │ │ │ │ └── edit_permission_panel.html │ │ │ │ │ ├── user │ │ │ │ │ │ ├── user_module.html │ │ │ │ │ │ ├── assign_role_panel.html │ │ │ │ │ │ ├── add_user_panel.html │ │ │ │ │ │ └── edit_user_panel.html │ │ │ │ │ ├── friend_link │ │ │ │ │ │ ├── friend_link_module.html │ │ │ │ │ │ ├── add_friend_link_panel.html │ │ │ │ │ │ └── edit_friend_link_panel.html │ │ │ │ │ ├── role │ │ │ │ │ │ ├── role_module.html │ │ │ │ │ │ ├── add_role_panel.html │ │ │ │ │ │ ├── edit_role_panel.html │ │ │ │ │ │ └── assign_permission_panel.html │ │ │ │ │ └── login_module.html │ │ │ │ ├── about_footer.html │ │ │ │ └── footer.html │ │ │ └── admin │ │ │ │ └── mail_preview.html │ │ └── application.yml │ └── java │ │ └── com │ │ └── hsuyeung │ │ └── blog │ │ ├── mapper │ │ ├── FileMapper.java │ │ ├── MailMapper.java │ │ ├── RoleMapper.java │ │ ├── UserMapper.java │ │ ├── UserRoleMapper.java │ │ ├── PermissionMapper.java │ │ ├── FriendLinkMapper.java │ │ ├── RolePermissionMapper.java │ │ ├── ArticleMapper.java │ │ ├── ContentMapper.java │ │ ├── CommentMapper.java │ │ └── SystemConfigMapper.java │ │ ├── constant │ │ ├── enums │ │ │ ├── ContentTypeEnum.java │ │ │ ├── ApiRateLimitStrategyEnum.java │ │ │ ├── PinEnum.java │ │ │ ├── LogicSwitchEnum.java │ │ │ ├── MailStatusEnum.java │ │ │ ├── MailTypeEnum.java │ │ │ ├── CommentNotificationSwitchEnum.java │ │ │ └── LogicDeleteEnum.java │ │ ├── CommonConstants.java │ │ ├── StringConstants.java │ │ ├── DateFormatConstants.java │ │ └── RegexConstants.java │ │ ├── BlogApplication.java │ │ ├── exception │ │ ├── NotFoundException.java │ │ ├── ApiRateLimitException.java │ │ ├── SystemInternalException.java │ │ ├── BizException.java │ │ └── GlobalControllerExceptionHandler.java │ │ ├── sitemap │ │ ├── ChangeFreqEnum.java │ │ └── URLNode.java │ │ ├── model │ │ ├── vo │ │ │ ├── comment │ │ │ │ ├── CommentReplyToVO.java │ │ │ │ ├── CommentUserVO.java │ │ │ │ ├── CommentTimeVO.java │ │ │ │ ├── CommentVO.java │ │ │ │ ├── CommentItemVO.java │ │ │ │ └── CommentInfoVO.java │ │ │ ├── article │ │ │ │ ├── ArchiveNode.java │ │ │ │ ├── ArchiveArticleVO.java │ │ │ │ ├── ArticleTitleInfoVO.java │ │ │ │ ├── HomeArticleVO.java │ │ │ │ ├── ArchiveVO.java │ │ │ │ ├── ArticleRouteAndTitleVO.java │ │ │ │ ├── ArticleDetailVO.java │ │ │ │ └── ArticleInfoVO.java │ │ │ ├── permission │ │ │ │ ├── EnabledPermissionVO.java │ │ │ │ ├── PermissionVO.java │ │ │ │ └── PermissionInfoVO.java │ │ │ ├── customconfig │ │ │ │ ├── AboutCustomConfigVO.java │ │ │ │ ├── ArchiveCustomConfigVO.java │ │ │ │ ├── FriendLinkCustomConfigVO.java │ │ │ │ ├── HomeCustomConfigVO.java │ │ │ │ └── CommonCustomConfigVO.java │ │ │ ├── PageVO.java │ │ │ ├── file │ │ │ │ └── FileInfoVO.java │ │ │ ├── role │ │ │ │ ├── EnabledRoleVO.java │ │ │ │ └── RoleInfoVO.java │ │ │ ├── friendlink │ │ │ │ ├── FriendLinkItemVO.java │ │ │ │ ├── FriendLinkVO.java │ │ │ │ ├── FriendLinkGroupVO.java │ │ │ │ └── FriendLinkInfoVO.java │ │ │ ├── user │ │ │ │ └── UserInfoVO.java │ │ │ ├── httpclient │ │ │ │ └── HttpClientResult.java │ │ │ ├── mail │ │ │ │ └── MailInfoVO.java │ │ │ └── systemconfig │ │ │ │ └── SystemConfigInfoVO.java │ │ ├── dto │ │ │ ├── role │ │ │ │ ├── RoleSearchDTO.java │ │ │ │ ├── CreateRoleRequestDTO.java │ │ │ │ ├── UpdateRoleRequestDTO.java │ │ │ │ ├── CreateRoleDTO.java │ │ │ │ └── UpdateRoleDTO.java │ │ │ ├── PageDTO.java │ │ │ ├── file │ │ │ │ └── FileSearchDTO.java │ │ │ ├── user │ │ │ │ ├── UserLoginRequestDTO.java │ │ │ │ ├── UserSearchDTO.java │ │ │ │ ├── UserLoginDTO.java │ │ │ │ ├── CreateUserRequestDTO.java │ │ │ │ ├── UpdateUserRequestDTO.java │ │ │ │ ├── CreateUserDTO.java │ │ │ │ └── UpdateUserDTO.java │ │ │ ├── PageSearchDTO.java │ │ │ ├── permission │ │ │ │ ├── PermissionSearchDTO.java │ │ │ │ ├── CreatePermissionRequestDTO.java │ │ │ │ ├── UpdatePermissionRequestDTO.java │ │ │ │ ├── CreatePermissionDTO.java │ │ │ │ └── UpdatePermissionDTO.java │ │ │ ├── systemconfig │ │ │ │ ├── SystemConfigSearchDTO.java │ │ │ │ ├── UpdateSystemConfigRequestDTO.java │ │ │ │ └── UpdateSystemConfigDTO.java │ │ │ ├── friendlink │ │ │ │ ├── FriendLinkSearchDTO.java │ │ │ │ ├── AddFriendLinkDTO.java │ │ │ │ └── UpdateFriendLinkDTO.java │ │ │ ├── article │ │ │ │ └── ArticleSearchDTO.java │ │ │ ├── mail │ │ │ │ └── MailSearchDTO.java │ │ │ └── comment │ │ │ │ ├── CommentSearchDTO.java │ │ │ │ ├── SubmitCommentDTO.java │ │ │ │ └── SubmitCommentRequestDTO.java │ │ └── entity │ │ │ ├── FileEntity.java │ │ │ ├── UserRoleEntity.java │ │ │ ├── RolePermissionEntity.java │ │ │ ├── ContentEntity.java │ │ │ ├── RoleEntity.java │ │ │ ├── UserEntity.java │ │ │ ├── PermissionEntity.java │ │ │ ├── FriendLinkEntity.java │ │ │ ├── SystemConfigEntity.java │ │ │ ├── ArticleEntity.java │ │ │ └── BaseEntity.java │ │ ├── rss │ │ └── Specification.java │ │ ├── service │ │ ├── IContentService.java │ │ ├── IRbacAuthorityService.java │ │ ├── IUserRoleService.java │ │ ├── IRolePermissionService.java │ │ ├── IUserTokenService.java │ │ └── impl │ │ │ └── ContentServiceImpl.java │ │ ├── web │ │ ├── core │ │ │ ├── WebResponse.java │ │ │ ├── RequestUserHolder.java │ │ │ └── HeaderMapRequestWrapper.java │ │ ├── api │ │ │ ├── UserTokenApi.java │ │ │ └── MailApi.java │ │ └── controller │ │ │ └── AdminPageController.java │ │ ├── config │ │ ├── properties │ │ │ ├── SecurityProperties.java │ │ │ └── RequestConfigProperties.java │ │ ├── ScheduleThreadPoolConfig.java │ │ ├── RequestConfigConfig.java │ │ ├── ThreadPoolConfig.java │ │ └── Swagger2Config.java │ │ ├── util │ │ ├── FileUtil.java │ │ ├── MD5Util.java │ │ ├── CommonUtil.java │ │ ├── JwtUtil.java │ │ └── ApplicationContextProvider.java │ │ ├── cache │ │ ├── lfu │ │ │ └── Node.java │ │ ├── FriendLinkCache.java │ │ └── CommentCache.java │ │ ├── annotation │ │ └── ApiRateLimit.java │ │ ├── schedule │ │ └── SendFailedEmailRetrySchedule.java │ │ ├── interceptor │ │ └── UserPermissionCheckInterceptor.java │ │ └── filter │ │ └── AddHeaderFilter.java └── test │ └── java │ └── com │ └── hsuyeung │ └── blog │ └── BlogApplicationTests.java ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── robots.txt └── .gitignore /src/main/resources/static/fonts/iconmoon/icomoon.eot: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/static/fonts/iconmoon/icomoon.ttf: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/static/fonts/iconmoon/icomoon.woff: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyeung/simple-blog/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | Crawl-delay: 5 4 | Sitemap: /sitemap.xml 5 | Host: www.hsuyeung.com 6 | -------------------------------------------------------------------------------- /src/main/resources/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyeung/simple-blog/HEAD/src/main/resources/static/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/static/img/pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyeung/simple-blog/HEAD/src/main/resources/static/img/pin.png -------------------------------------------------------------------------------- /src/main/resources/static/img/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyeung/simple-blog/HEAD/src/main/resources/static/img/avatar.jpg -------------------------------------------------------------------------------- /src/main/resources/static/img/err_img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyeung/simple-blog/HEAD/src/main/resources/static/img/err_img.jpg -------------------------------------------------------------------------------- /src/main/resources/static/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyeung/simple-blog/HEAD/src/main/resources/static/img/loading.gif -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/index.min.css: -------------------------------------------------------------------------------- 1 | .module-title{display:flex;display:-webkit-flex;justify-content:center;align-items:center} 2 | -------------------------------------------------------------------------------- /src/main/resources/static/img/about_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyeung/simple-blog/HEAD/src/main/resources/static/img/about_banner.png -------------------------------------------------------------------------------- /src/main/resources/static/img/home_banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyeung/simple-blog/HEAD/src/main/resources/static/img/home_banner.jpg -------------------------------------------------------------------------------- /src/main/resources/static/img/archive_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyeung/simple-blog/HEAD/src/main/resources/static/img/archive_banner.png -------------------------------------------------------------------------------- /src/main/resources/static/img/scrollbar-img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyeung/simple-blog/HEAD/src/main/resources/static/img/scrollbar-img.jpg -------------------------------------------------------------------------------- /src/main/resources/static/img/friend_link_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyeung/simple-blog/HEAD/src/main/resources/static/img/friend_link_banner.png -------------------------------------------------------------------------------- /src/main/resources/static/css/about.min.css: -------------------------------------------------------------------------------- 1 | .about-comment-bar{margin-top:4rem;border-bottom:.1rem solid rgba(154,128,92,.7)}.about-comment-bar .comment-title{display:inline} 2 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/file.min.css: -------------------------------------------------------------------------------- 1 | #file-module{border:.1rem solid rgba(154,128,92,.3)}#file-module-search-form{border-bottom:.1rem solid rgba(154,128,92,.3)} 2 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/mail.min.css: -------------------------------------------------------------------------------- 1 | #mail-module{border:.1rem solid rgba(154,128,92,.3)}#mail-module-search-form{border-bottom:.1rem solid rgba(154,128,92,.3)} 2 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/role.min.css: -------------------------------------------------------------------------------- 1 | #role-module{border:.1rem solid rgba(154,128,92,.3)}#role-module-search-form{border-bottom:.1rem solid rgba(154,128,92,.3)} 2 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/user.min.css: -------------------------------------------------------------------------------- 1 | #user-module{border:.1rem solid rgba(154,128,92,.3)}#user-module-search-form{border-bottom:.1rem solid rgba(154,128,92,.3)} 2 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/index.css: -------------------------------------------------------------------------------- 1 | .module-title { 2 | display: flex; 3 | display: -webkit-flex; 4 | justify-content: center; 5 | align-items: center; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/login.min.css: -------------------------------------------------------------------------------- 1 | #login-form{display:flex;display:-webkit-flex;justify-content:center;align-items:center;flex-direction:column;margin-top:25rem} 2 | -------------------------------------------------------------------------------- /src/main/resources/static/js/constants.js: -------------------------------------------------------------------------------- 1 | const EMAIL_REGEX = /^([A-Za-z\d_\-\.])+\@([A-Za-z\d_\-\.])+\.([A-Za-z]{2,8})$/ 2 | const NICKNAME_REGEX = /^[a-zA-Z\d\u4e00-\u9fa5]+$/ 3 | -------------------------------------------------------------------------------- /src/main/resources/static/css/article_list.min.css: -------------------------------------------------------------------------------- 1 | .article-list ul li{margin-top:.8rem}.article-list a:link,.article-list a:visited{color:#503214}.article-list a:hover{color:#ebab0a} 2 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/friend_link.min.css: -------------------------------------------------------------------------------- 1 | #friend-link-module{border:.1rem solid rgba(154,128,92,.3)}#friend-link-module-search-form{border-bottom:.1rem solid rgba(154,128,92,.3)} 2 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/permission.min.css: -------------------------------------------------------------------------------- 1 | #permission-module{border:.1rem solid rgba(154,128,92,.3)}#permission-module-search-form{border-bottom:.1rem solid rgba(154,128,92,.3)} 2 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/system_config.min.css: -------------------------------------------------------------------------------- 1 | #system-config-module{border:.1rem solid rgba(154,128,92,.3)}#system-config-module-search-form{border-bottom:.1rem solid rgba(154,128,92,.3)} 2 | -------------------------------------------------------------------------------- /src/main/resources/static/plugin/dompurify/dompurify.config.js: -------------------------------------------------------------------------------- 1 | DOMPurify.setConfig({ 2 | ADD_TAGS: ['iframe'], 3 | ADD_ATTR: ['target', 'allow', 'allowfullscreen', 'frameborder', 'scrolling'] 4 | }) 5 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/file.css: -------------------------------------------------------------------------------- 1 | #file-module { 2 | border: .1rem solid rgba(154, 128, 92, .3); 3 | } 4 | 5 | #file-module-search-form { 6 | border-bottom: .1rem solid rgba(154, 128, 92, .3); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/mail.css: -------------------------------------------------------------------------------- 1 | #mail-module { 2 | border: .1rem solid rgba(154, 128, 92, .3); 3 | } 4 | 5 | #mail-module-search-form { 6 | border-bottom: .1rem solid rgba(154, 128, 92, .3); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/role.css: -------------------------------------------------------------------------------- 1 | #role-module { 2 | border: .1rem solid rgba(154, 128, 92, .3); 3 | } 4 | 5 | #role-module-search-form { 6 | border-bottom: .1rem solid rgba(154, 128, 92, .3); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/user.css: -------------------------------------------------------------------------------- 1 | #user-module { 2 | border: .1rem solid rgba(154, 128, 92, .3); 3 | } 4 | 5 | #user-module-search-form { 6 | border-bottom: .1rem solid rgba(154, 128, 92, .3); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/static/css/about.css: -------------------------------------------------------------------------------- 1 | .about-comment-bar { 2 | margin-top: 4rem; 3 | border-bottom: .1rem solid rgba(154, 128, 92, .7) 4 | } 5 | 6 | .about-comment-bar .comment-title { 7 | display: inline 8 | } 9 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/login.css: -------------------------------------------------------------------------------- 1 | #login-form { 2 | display: flex; 3 | display: -webkit-flex; 4 | justify-content: center; 5 | align-items: center; 6 | flex-direction: column; 7 | margin-top: 25rem 8 | } 9 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/permission.css: -------------------------------------------------------------------------------- 1 | #permission-module { 2 | border: .1rem solid rgba(154, 128, 92, .3); 3 | } 4 | 5 | #permission-module-search-form { 6 | border-bottom: .1rem solid rgba(154, 128, 92, .3); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/static/css/footer.min.css: -------------------------------------------------------------------------------- 1 | footer{font-size:1.3rem;line-height:1.3;color:#8c704f;text-align:center;padding-top:3.6rem;padding-bottom:2.5rem}footer a:link,footer a:visited{color:#8c704f}footer a:hover{color:#ebab0a} 2 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/friend_link.css: -------------------------------------------------------------------------------- 1 | #friend-link-module { 2 | border: .1rem solid rgba(154, 128, 92, .3); 3 | } 4 | 5 | #friend-link-module-search-form { 6 | border-bottom: .1rem solid rgba(154, 128, 92, .3); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/system_config.css: -------------------------------------------------------------------------------- 1 | #system-config-module { 2 | border: .1rem solid rgba(154, 128, 92, .3); 3 | } 4 | 5 | #system-config-module-search-form { 6 | border-bottom: .1rem solid rgba(154, 128, 92, .3); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/static/css/header.min.css: -------------------------------------------------------------------------------- 1 | .nav .nav-menu{font-size:1.5rem;float:right}.nav .nav-item{margin-left:.4rem}@media (max-device-width:700px){.nav .nav-menu{display:block;float:none;margin-top:.5rem}.nav .nav-item{margin-left:0}} 2 | -------------------------------------------------------------------------------- /src/main/resources/static/css/index.min.css: -------------------------------------------------------------------------------- 1 | .more-article{font-weight:700}.article-publish-date{float:right;font-size:1.5rem;color:#8c704f}.pin{list-style:none url(../img/pin.png)}@media (max-device-width:700px){.article-publish-date{display:none}} 2 | -------------------------------------------------------------------------------- /src/main/resources/sql/07_init_user_role.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `t_user_role` (`uid`, `rid`, `create_by`, `create_time`, `update_by`, `update_time`, 2 | `is_deleted`) 3 | VALUES (1, 1, '1', '2022-07-03 20:27:28', '1', '2022-07-03 20:27:28', 0); 4 | -------------------------------------------------------------------------------- /src/main/resources/static/js/admin/constants.js: -------------------------------------------------------------------------------- 1 | /* 管理后台常量 */ 2 | const USERNAME_REGEX = /^[a-zA-Z]\w*$/ 3 | const ROLE_CODE_REGEX = /^[a-zA-Z]\w*$/ 4 | const PASSWORD_REGEX = /^[a-zA-Z][a-zA-Z_!@%\d]{7,15}$/ 5 | const HTTP_METHOD_REGEX = /^(get|post|put|delete)$/i 6 | -------------------------------------------------------------------------------- /src/main/resources/mapper/FileMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/mapper/MailMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/mapper/RoleMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/mapper/UserMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/static/css/article_list.css: -------------------------------------------------------------------------------- 1 | .article-list ul li { 2 | margin-top: .8rem 3 | } 4 | 5 | .article-list a:link, 6 | .article-list a:visited { 7 | color: #503214 8 | } 9 | 10 | .article-list a:hover { 11 | color: rgb(235, 171, 10, 1) 12 | } 13 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl = https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip 2 | wrapperUrl = https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 3 | -------------------------------------------------------------------------------- /src/main/resources/mapper/ArticleMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/mapper/CommentMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/mapper/ContentMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/mapper/UserRoleMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/comment.min.css: -------------------------------------------------------------------------------- 1 | #mail-module{border:.1rem solid rgba(154,128,92,.3)}#mail-module-search-form{border-bottom:.1rem solid rgba(154,128,92,.3)}select#search-comment-article{max-width:18rem}#comment-data-table-body .comment-avatar{width:6.4rem} 2 | -------------------------------------------------------------------------------- /src/main/resources/mapper/FriendLinkMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/mapper/PermissionMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/mapper/SystemConfigMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/sql/03_init_role.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `t_role` (`role_code`, `role_desc`, `is_enabled`, `create_by`, `create_time`, `update_by`, 2 | `update_time`, `is_deleted`) 3 | VALUES ('ADMIN', '管理员', 1, '1', '2022-07-01 09:29:38', '1', '2022-07-03 21:16:56', 0); 4 | -------------------------------------------------------------------------------- /src/main/resources/mapper/RolePermissionMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/article.min.css: -------------------------------------------------------------------------------- 1 | #article-module{border:.1rem solid rgba(154,128,92,.3)}#article-module-search-form{border-bottom:.1rem solid rgba(154,128,92,.3)}#add-article-content,#edit-article-content{background:#fff;width:76.8rem;margin:1rem 0 0 .6rem;padding:.8rem 1.2rem} 2 | -------------------------------------------------------------------------------- /src/test/java/com/hsuyeung/blog/BlogApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class BlogApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/sql/06_init_user.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `t_user` (`username`, `password`, `nickname`, `is_enabled`, `create_by`, `create_time`, `update_by`, 2 | `update_time`, `is_deleted`) 3 | VALUES ('admin', '10c5c4007004067f10441397cd5f9247', '管理员', 1, '1', '2022-06-30 17:26:36', '1', 4 | '2022-07-03 16:17:42', 0); 5 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/mapper/FileMapper.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.hsuyeung.blog.model.entity.FileEntity; 5 | 6 | /** 7 | * @author hsuyeung 8 | * @date 2022/06/27 9 | */ 10 | public interface FileMapper extends BaseMapper { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/mapper/MailMapper.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.hsuyeung.blog.model.entity.MailEntity; 5 | 6 | /** 7 | * @author hsuyeung 8 | * @date 2022/06/18 9 | */ 10 | public interface MailMapper extends BaseMapper { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/resources/static/css/footer.css: -------------------------------------------------------------------------------- 1 | footer { 2 | font-size: 1.3rem; 3 | line-height: 1.3; 4 | color: #8c704f; 5 | text-align: center; 6 | padding-top: 3.6rem; 7 | padding-bottom: 2.5rem 8 | } 9 | 10 | footer a:link, 11 | footer a:visited { 12 | color: #8c704f 13 | } 14 | 15 | footer a:hover { 16 | color: rgb(235, 171, 10, 1) 17 | } 18 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/comment.css: -------------------------------------------------------------------------------- 1 | #mail-module { 2 | border: .1rem solid rgba(154, 128, 92, .3); 3 | } 4 | 5 | #mail-module-search-form { 6 | border-bottom: .1rem solid rgba(154, 128, 92, .3); 7 | } 8 | 9 | select#search-comment-article { 10 | max-width: 18rem; 11 | } 12 | 13 | #comment-data-table-body .comment-avatar { 14 | width: 6.4rem; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/mapper/RoleMapper.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.hsuyeung.blog.model.entity.RoleEntity; 5 | 6 | /** 7 | * 角色 Mapper 8 | * 9 | * @author hsuyeung 10 | * @date 2022/06/28 11 | */ 12 | public interface RoleMapper extends BaseMapper { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.hsuyeung.blog.model.entity.UserEntity; 5 | 6 | /** 7 | * 用户 Mapper 8 | * 9 | * @author hsuyeung 10 | * @date 2022/06/28 11 | */ 12 | public interface UserMapper extends BaseMapper { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/constant/enums/ContentTypeEnum.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.constant.enums; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | /** 7 | * 内容类型枚举 8 | * 9 | * @author hsuyeung 10 | * @date 2022/06/06 11 | */ 12 | @Getter 13 | @AllArgsConstructor 14 | public enum ContentTypeEnum { 15 | HTML, 16 | MARKDOWN 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/constant/enums/ApiRateLimitStrategyEnum.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.constant.enums; 2 | 3 | /** 4 | * 请求速率限制策略枚举 5 | * 6 | * @author hsuyeung 7 | * @date 2022/06/17 8 | */ 9 | public enum ApiRateLimitStrategyEnum { 10 | /** 11 | * 根据请求的 ip 地址进行限制 12 | */ 13 | IP, 14 | /** 15 | * 根据用户账户进行限制 16 | */ 17 | USER 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/mapper/UserRoleMapper.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.hsuyeung.blog.model.entity.UserRoleEntity; 5 | 6 | /** 7 | * 用户-角色 Mapper 8 | * 9 | * @author hsuyeung 10 | * @date 2022/06/28 11 | */ 12 | public interface UserRoleMapper extends BaseMapper { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/mapper/PermissionMapper.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.hsuyeung.blog.model.entity.PermissionEntity; 5 | 6 | /** 7 | * 权限 Mapper 8 | * 9 | * @author hsuyeung 10 | * @date 2022/06/28 11 | */ 12 | public interface PermissionMapper extends BaseMapper { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/static/css/article.min.css: -------------------------------------------------------------------------------- 1 | .back-home{font-size:1.5rem;border-bottom:.1rem solid rgba(154,128,92,.7);display:block}.article-title-container{padding:0 0 1.5rem}.article-meta{margin-bottom:2.8rem;font-size:1.5rem}.article-sign{text-align:right;margin-top:4.5rem;font-size:1.5rem}.article-sign,.article-sign a:link,.article-sign a:visited{color:#8c704f}.article-sign a:hover{color:#ebab0a} 2 | -------------------------------------------------------------------------------- /src/main/resources/static/css/index.css: -------------------------------------------------------------------------------- 1 | .more-article { 2 | font-weight: 700 3 | } 4 | 5 | .article-publish-date { 6 | float: right; 7 | font-size: 1.5rem; 8 | color: #8c704f 9 | } 10 | 11 | .pin { 12 | list-style: none url('../img/pin.png'); 13 | } 14 | 15 | @media (max-device-width: 700px),(max-width: 700px) { 16 | .article-publish-date { 17 | display: none 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/mapper/FriendLinkMapper.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.hsuyeung.blog.model.entity.FriendLinkEntity; 5 | 6 | /** 7 | * 友链 Mapper 接口 8 | * 9 | * @author hsuyeung 10 | * @date 2022/06/22 11 | */ 12 | public interface FriendLinkMapper extends BaseMapper { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/sql/09_20230106_增加开往社区链接配置.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `t_system_config` (`conf_key`, `conf_value`, `conf_group`, `description`, `is_enabled`, `create_by`, 2 | `create_time`, `update_by`, `update_time`, `is_deleted`) 3 | VALUES ('travellingsLink', 'https://www.travellings.cn/go.html', 'custom', '开往社区的链接', 1, '1', 4 | '2023-01-06 15:07:00', '1', '2023-01-06 15:07:00', 0); 5 | -------------------------------------------------------------------------------- /src/main/resources/static/css/admin/article.css: -------------------------------------------------------------------------------- 1 | #article-module { 2 | border: .1rem solid rgba(154, 128, 92, .3); 3 | } 4 | 5 | #article-module-search-form { 6 | border-bottom: .1rem solid rgba(154, 128, 92, .3); 7 | } 8 | 9 | #edit-article-content, 10 | #add-article-content { 11 | background: white; 12 | width: 76.8rem; 13 | margin: 1rem 0 0 .6rem; 14 | padding: 0.8rem 1.2rem; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/static/css/header.css: -------------------------------------------------------------------------------- 1 | .nav .nav-menu { 2 | font-size: 1.5rem; 3 | float: right 4 | } 5 | 6 | .nav .nav-item { 7 | margin-left: .4rem 8 | } 9 | 10 | @media (max-device-width: 700px),(max-width: 700px) { 11 | .nav .nav-menu { 12 | display: block; 13 | float: none; 14 | margin-top: .5rem 15 | } 16 | 17 | .nav .nav-item { 18 | margin-left: 0 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/mapper/RolePermissionMapper.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.hsuyeung.blog.model.entity.RolePermissionEntity; 5 | 6 | /** 7 | * 角色-权限 Mapper 8 | * 9 | * @author hsuyeung 10 | * @date 2022/06/28 11 | */ 12 | public interface RolePermissionMapper extends BaseMapper { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/file/file_module.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

文件上传记录管理

4 |
5 |
6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/mail/mail_module.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

邮件发送记录管理

4 |
5 |
6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/mapper/ArticleMapper.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.hsuyeung.blog.model.entity.ArticleEntity; 5 | 6 | /** 7 | *

8 | * 博客文章表 Mapper 接口 9 | *

10 | * 11 | * @author hsuyeung 12 | * @since 2022/06/05 13 | */ 14 | public interface ArticleMapper extends BaseMapper { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/mapper/ContentMapper.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.hsuyeung.blog.model.entity.ContentEntity; 5 | 6 | /** 7 | *

8 | * 文章内容表 Mapper 接口 9 | *

10 | * 11 | * @author hsuyeung 12 | * @since 2022/06/05 13 | */ 14 | public interface ContentMapper extends BaseMapper { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/mapper/CommentMapper.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.mapper; 2 | 3 | 4 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 5 | import com.hsuyeung.blog.model.entity.CommentEntity; 6 | 7 | /** 8 | *

9 | * 文章评论 Mapper 接口 10 | *

11 | * 12 | * @author hsuyeung 13 | * @since 2022/06/05 14 | */ 15 | public interface CommentMapper extends BaseMapper { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/comment/comment_module.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

评论管理

4 |
5 |
6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/mapper/SystemConfigMapper.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.hsuyeung.blog.model.entity.SystemConfigEntity; 5 | 6 | /** 7 | *

8 | * 系统配置表 Mapper 接口 9 | *

10 | * 11 | * @author hsuyeung 12 | * @since 2022/06/05 13 | */ 14 | public interface SystemConfigMapper extends BaseMapper { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/BlogApplication.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * @author hsuyeung 8 | */ 9 | @SpringBootApplication 10 | public class BlogApplication { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(BlogApplication.class, args); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/constant/CommonConstants.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.constant; 2 | 3 | /** 4 | * 公共常量 5 | * 6 | * @author hsuyeung 7 | * @date 2022/12/05 8 | */ 9 | public final class CommonConstants { 10 | /** 11 | * 1KB 12 | */ 13 | public static final int ONE_KB = 1024 * Byte.BYTES; 14 | /** 15 | * 1MB 16 | */ 17 | public static final int ONE_MB = 1024 * ONE_KB; 18 | 19 | private CommonConstants() { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/about_footer.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | avatar 5 | 6 | 7 |
8 |
9 | 联系我 10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /src/main/resources/templates/admin/mail_preview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 邮件内容预览 | Hsu Yeung 的博客 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/article/preview_article_panel.html: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /src/main/resources/sql/08_20221101_增加失败邮件重发功能.sql: -------------------------------------------------------------------------------- 1 | -- 增加邮件发送失败最大重试次数系统配置 2 | INSERT INTO `t_system_config` (`conf_key`, `conf_value`, `conf_group`, `description`, `is_enabled`, 3 | `create_by`, `create_time`, `update_by`, `update_time`, `is_deleted`) 4 | VALUES ('mailRetryMaxNum', '3', 'system', '发送失败的邮件最大重试次数', 1, '1', '2022-10-31 19:26:34', '1', 5 | '2022-10-31 19:26:34', 0); 6 | -- 新增邮件失败重试次数字段 7 | ALTER TABLE `t_mail` 8 | ADD COLUMN `retry_num` tinyint NOT NULL DEFAULT 0 COMMENT '失败重试次数' AFTER `send_time`; 9 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/constant/StringConstants.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.constant; 2 | 3 | /** 4 | * 字符串常量 5 | * 6 | * @author hsuyeung 7 | * @date 2022/11/01 8 | */ 9 | public final class StringConstants { 10 | public static final String SYSTEM = "system"; 11 | public static final String RSS_FILE_PATH = "./rss.xml"; 12 | public static final String ROBOTS_FILE_PATH = "./robots.txt"; 13 | public static final String SITEMAP_FILE_PATH = "./sitemap.xml"; 14 | 15 | private StringConstants() { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/exception/NotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.exception; 2 | 3 | /** 4 | * 资源未找到的异常 5 | * 6 | * @author hsuyeung 7 | * @date 2022/06/20 8 | */ 9 | public class NotFoundException extends RuntimeException { 10 | private static final long serialVersionUID = -6135782581028401974L; 11 | 12 | public NotFoundException(String message) { 13 | this(message, null); 14 | } 15 | 16 | public NotFoundException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | /log/ 35 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/exception/ApiRateLimitException.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.exception; 2 | 3 | /** 4 | * api 请求速率被限制时抛出的异常 5 | * 6 | * @author hsuyeung 7 | * @date 2022/06/17 8 | */ 9 | public class ApiRateLimitException extends RuntimeException { 10 | private static final long serialVersionUID = -5040009887655650906L; 11 | 12 | public ApiRateLimitException(String message) { 13 | this(message, null); 14 | } 15 | 16 | public ApiRateLimitException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/sitemap/ChangeFreqEnum.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.sitemap; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | /** 7 | * 站点地图抓取频率枚举 8 | * 9 | * @author hsuyeung 10 | * @date 2023/11/10 11 | */ 12 | @Getter 13 | @AllArgsConstructor 14 | public enum ChangeFreqEnum { 15 | 16 | ALWAYS("always"), 17 | HOURLY("hourly"), 18 | DAILY("daily"), 19 | WEEKLY("weekly"), 20 | MONTHLY("monthly"), 21 | YEARLY("yearly"), 22 | NEVER("never"), 23 | ; 24 | 25 | private final String value; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/resources/static/css/article.css: -------------------------------------------------------------------------------- 1 | .back-home { 2 | font-size: 1.5rem; 3 | border-bottom: .1rem solid rgba(154, 128, 92, .7); 4 | display: block 5 | } 6 | 7 | .article-title-container { 8 | padding: 0 0 1.5rem 0 9 | } 10 | 11 | .article-meta { 12 | margin-bottom: 2.8rem; 13 | font-size: 1.5rem 14 | } 15 | 16 | .article-sign { 17 | text-align: right; 18 | margin-top: 4.5rem; 19 | font-size: 1.5rem 20 | } 21 | 22 | .article-sign, 23 | .article-sign a:link, 24 | .article-sign a:visited { 25 | color: #8c704f 26 | } 27 | 28 | .article-sign a:hover { 29 | color: rgb(235, 171, 10, 1) 30 | } 31 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/system_config/system_config_module.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

系统配置管理

4 |
5 |
6 |
7 | 8 | 9 |
10 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/constant/enums/PinEnum.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.constant.enums; 2 | 3 | import com.baomidou.mybatisplus.annotation.EnumValue; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | 7 | /** 8 | * 是否置顶枚举 9 | * 10 | * @author hsuyeung 11 | * @date 2022/07/26 12 | */ 13 | @Getter 14 | @AllArgsConstructor 15 | public enum PinEnum { 16 | /** 17 | * 未置顶 18 | */ 19 | UN_PIN(0, "未置顶"), 20 | 21 | /** 22 | * 置顶 23 | */ 24 | PIN(1, "置顶"); 25 | 26 | @EnumValue 27 | private final Integer code; 28 | private final String desc; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/constant/enums/LogicSwitchEnum.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.constant.enums; 2 | 3 | import com.baomidou.mybatisplus.annotation.EnumValue; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | 7 | /** 8 | * 逻辑开关枚举 9 | * 10 | * @author hsuyeung 11 | * @date 2022/06/28 12 | */ 13 | @Getter 14 | @AllArgsConstructor 15 | public enum LogicSwitchEnum { 16 | 17 | /** 18 | * 关闭 19 | */ 20 | OFF(0, "关闭"), 21 | 22 | /** 23 | * 开启 24 | */ 25 | ON(1, "开启"); 26 | 27 | @EnumValue 28 | private final Integer code; 29 | private final String desc; 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/comment/CommentReplyToVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.comment; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.Data; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * 用来表示二级评论回复的谁 10 | * 11 | * @author hsuyeung 12 | * @date 2022/06/13 13 | */ 14 | @Data 15 | public class CommentReplyToVO implements Serializable { 16 | private static final long serialVersionUID = -2216990859462394070L; 17 | 18 | @ApiModelProperty("被回复的评论的 id") 19 | private Long commentId; 20 | 21 | @ApiModelProperty("被回复人的昵称") 22 | private String nickname; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/static/plugin/marked/marked.renderer.js: -------------------------------------------------------------------------------- 1 | /* 对 marked.js 的渲染规则改写 */ 2 | const renderer = { 3 | image(href, title, text) { 4 | return `
${text ? text : "default-alt"}
${text}
` 5 | }, 6 | link(href, title, text) { 7 | return `${text}` 8 | }, 9 | heading(text, level) { 10 | // 生成 ID 11 | const uuid = crypto.randomUUID(); 12 | return `${text}`; 13 | } 14 | } 15 | 16 | marked.use({renderer}) 17 | -------------------------------------------------------------------------------- /src/main/resources/static/js/utils.js: -------------------------------------------------------------------------------- 1 | function subAndAppend(str, len, append) { 2 | if (typeof str !== 'string') { 3 | return '' 4 | } 5 | if (str.length <= len) { 6 | return str 7 | } 8 | return str.substring(0, len) + append 9 | } 10 | 11 | function encode(str) { 12 | return encodeURIComponent(str) 13 | .replace(/'/g, '%27') 14 | .replace(/\(/g, '%28') 15 | .replace(/\)/g, '%29'); 16 | } 17 | 18 | function decode(str) { 19 | return decodeURIComponent(str) 20 | .replace(/%27/g, "'") 21 | .replace(/%22/g, '"') 22 | .replace(/%28/g, '(') 23 | .replace(/%29/g, ')'); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/article/ArchiveNode.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.article; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.Data; 5 | 6 | import java.io.Serializable; 7 | import java.util.List; 8 | 9 | /** 10 | * 文章归档页面一个结点的数据 11 | * 12 | * @author hsuyeung 13 | * @date 2022/06/07 14 | */ 15 | @Data 16 | public class ArchiveNode implements Serializable { 17 | private static final long serialVersionUID = -1031801582904665449L; 18 | 19 | @ApiModelProperty("时间") 20 | private String time; 21 | 22 | @ApiModelProperty("该结点下的文章列表") 23 | private List articleList; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/permission/permission_module.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

权限管理

4 |
5 |
6 |
7 |
8 | 9 | 10 |
11 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/comment/CommentUserVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.comment; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.Data; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * 评论者信息实体类 10 | * 11 | * @author hsuyeung 12 | * @date 2022/06/13 13 | */ 14 | @Data 15 | public class CommentUserVO implements Serializable { 16 | private static final long serialVersionUID = 220875395425392864L; 17 | 18 | @ApiModelProperty("昵称") 19 | private String nickname; 20 | 21 | @ApiModelProperty("头像") 22 | private String avatar; 23 | 24 | @ApiModelProperty("网站") 25 | private String website; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/user/user_module.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

用户管理

4 |
5 |
6 |
7 |
8 |
9 | 10 | 11 |
12 | -------------------------------------------------------------------------------- /src/main/resources/static/css/iconmoon.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:icomoon;src:url(../fonts/iconmoon/icomoon.eot?k18w5r);src:url(../fonts/iconmoon/icomoon.eot?k18w5r#iefix) format("embedded-opentype"),url(../fonts/iconmoon/icomoon.ttf?k18w5r) format("truetype"),url(../fonts/iconmoon/icomoon.woff?k18w5r) format("woff"),url(../fonts/iconmoon/icomoon.svg?k18w5r#icomoon) format("svg");font-weight:400;font-style:normal;font-display:block}[class*=" icon-"],[class^=icon-]{font-family:icomoon!important;speak:never;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-mail:before{content:"\e900"} 2 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/friend_link/friend_link_module.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/role/role_module.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

角色管理

4 |
5 |
6 |
7 |
8 |
9 | 10 | 11 |
12 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/article/ArchiveArticleVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.article; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.Data; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * 归档页面文章信息 10 | * 11 | * @author hsuyeung 12 | * @date 2022/06/07 13 | */ 14 | @Data 15 | public class ArchiveArticleVO implements Serializable { 16 | private static final long serialVersionUID = 7543957697871029618L; 17 | 18 | @ApiModelProperty("文章路由") 19 | private String route; 20 | 21 | @ApiModelProperty("文章标题") 22 | private String title; 23 | 24 | @ApiModelProperty("创建时间") 25 | private String createTime; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/article/ArticleTitleInfoVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.article; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * 文章标题信息 11 | * 12 | * @author hsuyeung 13 | * @date 2022/07/07 14 | */ 15 | @ApiModel(description = "文章标题信息") 16 | @Data 17 | public class ArticleTitleInfoVO implements Serializable { 18 | private static final long serialVersionUID = 3559305142735970864L; 19 | 20 | @ApiModelProperty("文章 id") 21 | private Long id; 22 | 23 | @ApiModelProperty("文章标题") 24 | private String title; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/resources/static/css/about_footer.min.css: -------------------------------------------------------------------------------- 1 | .about{display:block;padding-top:1.4rem;border-top:.1rem solid rgba(154,128,92,.2);margin-top:1.6rem;max-width:100%;margin-bottom:-3.2rem;font-size:1.5rem}.about-avatar{float:left}.avatar{display:inline;width:2.3rem;height:2.3rem}.about-text{margin-left:1rem;vertical-align:top}.about-nav{float:right}.about a:link,.about a:visited,.nav a:link,.nav a:visited{color:#503214}.about a:hover,.nav a:hover{color:#ebab0a}.about [class*=" icon-"],.about [class^=icon-]{margin-right:.2rem}.sponsor{text-align:center;margin:4.5rem auto}.sponsor img.qrcode{border:.3rem solid #e3ca85;max-width:18.9rem}@media (max-device-width:700px){.about{display:none}}@media print{.sponsor{display:none}} 2 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/comment/CommentTimeVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.comment; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.Data; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * 评论时间信息实体类 10 | * 11 | * @author hsuyeung 12 | * @date 2022/06/13 13 | */ 14 | @Data 15 | public class CommentTimeVO implements Serializable { 16 | private static final long serialVersionUID = -6741531752802668513L; 17 | 18 | @ApiModelProperty("格式化的时间") 19 | private String formattedTime; 20 | 21 | @ApiModelProperty("未格式化的时间") 22 | private String standardTime; 23 | 24 | @ApiModelProperty("美化后的时间") 25 | private String beautifyTime; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/permission/EnabledPermissionVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.permission; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * 可用状态的权限信息 11 | * 12 | * @author hsuyeung 13 | * @date 2022/07/03 14 | */ 15 | @ApiModel(description = "可用状态的权限信息") 16 | @Data 17 | public class EnabledPermissionVO implements Serializable { 18 | private static final long serialVersionUID = 1295131438818399160L; 19 | 20 | @ApiModelProperty("权限 id") 21 | private Long id; 22 | 23 | @ApiModelProperty("权限描述") 24 | private String permissionDesc; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/exception/SystemInternalException.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.exception; 2 | 3 | import lombok.Data; 4 | import lombok.EqualsAndHashCode; 5 | 6 | /** 7 | * 系统内部异常 8 | *

这种异常不应该返回给客户端展示,统一返回系统繁忙并记录异常日志

9 | * 10 | * @author hsuyeung 11 | * @date 2022/05/25 12 | */ 13 | @Data 14 | @EqualsAndHashCode(callSuper = true) 15 | public class SystemInternalException extends RuntimeException { 16 | private static final long serialVersionUID = -6887627022008901460L; 17 | 18 | public SystemInternalException(String msg) { 19 | this(msg, null); 20 | } 21 | 22 | public SystemInternalException(String msg, Throwable cause) { 23 | super(msg, cause); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/rss/Specification.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.rss; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.FIELD; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | /** 11 | * RSS 字段定义 12 | * 13 | * @author hsuyeung 14 | * @date 2023/03/04 15 | */ 16 | @Target(FIELD) 17 | @Retention(RUNTIME) 18 | @Documented 19 | public @interface Specification { 20 | /** 21 | * 字段是否必填,默认 false 22 | */ 23 | boolean required() default false; 24 | 25 | /** 26 | * 字段描述 27 | */ 28 | String description() default ""; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/customconfig/AboutCustomConfigVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.customconfig; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.io.Serializable; 9 | 10 | /** 11 | * 关于页面自定义配置 12 | * 13 | * @author hsuyeung 14 | * @date 2022/06/22 15 | */ 16 | @Data 17 | @Builder 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | public class AboutCustomConfigVO implements Serializable { 21 | private static final long serialVersionUID = -7942228497618235935L; 22 | 23 | private String blogAboutDesc; 24 | private String blogAboutKeywords; 25 | private String blogAboutBannerImg; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/service/IContentService.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.hsuyeung.blog.constant.enums.ContentTypeEnum; 5 | import com.hsuyeung.blog.model.entity.ContentEntity; 6 | 7 | /** 8 | *

9 | * 文章内容表 服务类 10 | *

11 | * 12 | * @author hsuyeung 13 | * @since 2022/06/05 14 | */ 15 | public interface IContentService extends IService { 16 | 17 | /** 18 | * 根据内容 id 和内容类型获取内容 19 | * 20 | * @param contentId 内容 id 21 | * @param contentType {@link ContentTypeEnum} 22 | * @return 内容 23 | */ 24 | String getContent(Long contentId, ContentTypeEnum contentType); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/web/core/WebResponse.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.web.core; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.experimental.Accessors; 8 | 9 | /** 10 | * API 接口返回数据统一格式 11 | * 12 | * @author hsuyeung 13 | * @date 2022/02/22 14 | */ 15 | @JsonInclude(JsonInclude.Include.NON_NULL) 16 | @Data 17 | @Accessors(chain = true) 18 | @Builder 19 | public class WebResponse { 20 | @ApiModelProperty("状态码") 21 | private Integer code; 22 | 23 | @ApiModelProperty("提示信息") 24 | private String msg; 25 | 26 | @ApiModelProperty("返回数据") 27 | private T data; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/customconfig/ArchiveCustomConfigVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.customconfig; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.io.Serializable; 9 | 10 | /** 11 | * 归档页面自定义配置 12 | * 13 | * @author hsuyeung 14 | * @date 2022/06/22 15 | */ 16 | @Data 17 | @Builder 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | public class ArchiveCustomConfigVO implements Serializable { 21 | private static final long serialVersionUID = -8302135040002502246L; 22 | 23 | private String blogArchiveDesc; 24 | private String blogArchiveKeywords; 25 | private String blogArchiveBannerImg; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/PageVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.io.Serializable; 9 | import java.util.List; 10 | 11 | /** 12 | * 分页数据 13 | * 14 | * @author hsuyeung 15 | * @date 2022/05/16 16 | */ 17 | @Data 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | public class PageVO implements Serializable { 21 | private static final long serialVersionUID = 3013788305392199918L; 22 | 23 | @ApiModelProperty("总数") 24 | private Long total; 25 | 26 | @ApiModelProperty("数据列表") 27 | private List data; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/customconfig/FriendLinkCustomConfigVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.customconfig; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.io.Serializable; 9 | 10 | /** 11 | * 友链页面自定义配置信息 12 | * 13 | * @author hsuyeung 14 | * @date 2022/06/23 15 | */ 16 | @Data 17 | @Builder 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | public class FriendLinkCustomConfigVO implements Serializable { 21 | private static final long serialVersionUID = -7440234097998280417L; 22 | 23 | private String friendLinkDesc; 24 | private String friendLinkKeywords; 25 | private String friendLinkBannerImg; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/article/HomeArticleVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.article; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.Data; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * 博客首页文章列表实体类 10 | * 11 | * @author hsuyeung 12 | * @date 2022/06/05 13 | */ 14 | @Data 15 | public class HomeArticleVO implements Serializable { 16 | private static final long serialVersionUID = 4060356956458607980L; 17 | 18 | @ApiModelProperty("文章路由") 19 | private String route; 20 | 21 | @ApiModelProperty("文章标题") 22 | private String title; 23 | 24 | @ApiModelProperty("创建时间") 25 | private String createTime; 26 | 27 | @ApiModelProperty("是否置顶") 28 | private Boolean pin; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/file/FileInfoVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.file; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * 分页列表文件信息 11 | * 12 | * @author hsuyeung 13 | * @date 2022/07/07 14 | */ 15 | @ApiModel(description = "分页列表文件信息") 16 | @Data 17 | public class FileInfoVO implements Serializable { 18 | private static final long serialVersionUID = -2075427838065008705L; 19 | 20 | @ApiModelProperty("文件访问全路径") 21 | private String url; 22 | 23 | @ApiModelProperty("上传时间") 24 | private String createTime; 25 | 26 | @ApiModelProperty("上传人") 27 | private String createBy; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/login_module.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | 8 | 9 |
10 |
11 | 12 | 13 |
14 |
15 | 16 |
17 | 18 | 19 |
20 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/customconfig/HomeCustomConfigVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.customconfig; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.io.Serializable; 9 | 10 | /** 11 | * 博客首页自定义配置 12 | * 13 | * @author hsuyeung 14 | * @date 2022/06/22 15 | */ 16 | @Data 17 | @Builder 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | public class HomeCustomConfigVO implements Serializable { 21 | private static final long serialVersionUID = 5628038802254781213L; 22 | 23 | private String blogHomeTitle; 24 | private String blogHomeDesc; 25 | private String blogHomeKeywords; 26 | private String blogHomeBannerImg; 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/role/EnabledRoleVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.role; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * 启用状态的角色信息实体类 11 | * 12 | * @author hsuyeung 13 | * @date 2022/07/02 14 | */ 15 | @ApiModel(description = "启用状态的角色信息") 16 | @Data 17 | public class EnabledRoleVO implements Serializable { 18 | private static final long serialVersionUID = -5510485238794039162L; 19 | 20 | @ApiModelProperty("角色 id") 21 | private Long id; 22 | 23 | @ApiModelProperty("角色编码") 24 | private String roleCode; 25 | 26 | @ApiModelProperty("角色描述") 27 | private String roleDesc; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/comment/CommentVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.comment; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.io.Serializable; 9 | import java.util.List; 10 | 11 | /** 12 | * 评论列表 13 | * 14 | * @author hsuyeung 15 | * @date 2022/06/14 16 | */ 17 | @Data 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | public class CommentVO implements Serializable { 21 | private static final long serialVersionUID = 5312924321193733968L; 22 | 23 | @ApiModelProperty("总数") 24 | private Long totalSize; 25 | 26 | @ApiModelProperty("评论列表") 27 | private List commentItemList; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/role/add_role_panel.html: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/role/RoleSearchDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.role; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiParam; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.Size; 8 | import java.io.Serializable; 9 | 10 | /** 11 | * 角色分页搜条件 12 | * 13 | * @author hsuyeung 14 | * @since 2023/8/26 15 | */ 16 | @Data 17 | @ApiModel(description = "角色分页搜条件") 18 | public class RoleSearchDTO implements Serializable { 19 | private static final long serialVersionUID = -7386403798748707272L; 20 | 21 | @ApiParam("角色编码") 22 | @Size(max = 64, message = "角色编码不能超过 64 个字符") 23 | private String roleCode; 24 | 25 | @ApiParam("是否可用") 26 | private Boolean enabled; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/article/ArchiveVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.article; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.io.Serializable; 9 | import java.util.List; 10 | 11 | /** 12 | * 归档信息 13 | * 14 | * @author hsuyeung 15 | * @date 2022/06/07 16 | */ 17 | @Data 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | public class ArchiveVO implements Serializable { 21 | private static final long serialVersionUID = -6868588806411146073L; 22 | 23 | @ApiModelProperty("总数") 24 | private Integer totalSize; 25 | 26 | @ApiModelProperty("归档结点列表") 27 | private List archiveNodeList; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/friendlink/FriendLinkItemVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.friendlink; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.Data; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * 单个友链对象实体类 10 | * 11 | * @author hsuyeung 12 | * @date 2022/06/23 13 | */ 14 | @Data 15 | public class FriendLinkItemVO implements Serializable { 16 | private static final long serialVersionUID = 9002959025776205204L; 17 | 18 | @ApiModelProperty("友链名称") 19 | private String linkName; 20 | 21 | @ApiModelProperty("友链链接") 22 | private String linkUrl; 23 | 24 | @ApiModelProperty("友链头像") 25 | private String linkAvatar; 26 | 27 | @ApiModelProperty("一句话描述") 28 | private String linkDesc; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/customconfig/CommonCustomConfigVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.customconfig; 2 | 3 | import lombok.*; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * 页面自定义配置基类 9 | * 10 | * @author hsuyeung 11 | * @date 2022/06/22 12 | */ 13 | @Data 14 | @Builder 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | @EqualsAndHashCode 18 | public class CommonCustomConfigVO implements Serializable { 19 | private static final long serialVersionUID = 5411769546269514995L; 20 | 21 | private String headerText; 22 | private String aboutFooterText; 23 | private String footerCopyright; 24 | private String footerAboutText; 25 | private String beianNum; 26 | private String avatar; 27 | private String travellingsLink; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/friendlink/FriendLinkVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.friendlink; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.io.Serializable; 9 | import java.util.List; 10 | 11 | /** 12 | * 友链信息 13 | * 14 | * @author hsuyeung 15 | * @date 2022/06/23 16 | */ 17 | @Data 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | public class FriendLinkVO implements Serializable { 21 | private static final long serialVersionUID = -1619323604291445940L; 22 | 23 | @ApiModelProperty("友链总数") 24 | private Long totalSize; 25 | 26 | @ApiModelProperty("友链分组列表") 27 | private List friendLinkGroupList; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/friendlink/FriendLinkGroupVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.friendlink; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.io.Serializable; 9 | import java.util.List; 10 | 11 | /** 12 | * 友链分组信息实体类 13 | * 14 | * @author hsuyeung 15 | * @date 2022/06/23 16 | */ 17 | @Data 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | public class FriendLinkGroupVO implements Serializable { 21 | private static final long serialVersionUID = 3655445511565082341L; 22 | 23 | @ApiModelProperty("友链分组") 24 | private String linkGroup; 25 | 26 | @ApiModelProperty("友链列表") 27 | private List friendLinkItemList; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/config/properties/SecurityProperties.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.config.properties; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * 接口安全配置 11 | * 12 | * @author hsuyeung 13 | * @date 2022/06/28 14 | */ 15 | @Data 16 | @Configuration 17 | @ConfigurationProperties(prefix = "security.config") 18 | public class SecurityProperties { 19 | /** 20 | * 权限放行列表 21 | */ 22 | private List excludePathPatterns; 23 | 24 | /** 25 | * 权限拦截列表 26 | */ 27 | private List pathPatterns; 28 | 29 | /** 30 | * 用户 token 名字 31 | */ 32 | private String tokenName = "token"; 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/PageDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | import javax.validation.constraints.Min; 11 | 12 | /** 13 | * 分页参数 14 | * 15 | * @author hsuyeung 16 | * @date 2022/11/04 17 | */ 18 | @Data 19 | @Builder 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | @ApiModel(description = "分页参数") 23 | public class PageDTO { 24 | @ApiModelProperty("页码") 25 | @Min(value = 1, message = "页码不能小于 0") 26 | private Integer pageNum; 27 | 28 | @ApiModelProperty("页大小") 29 | @Min(value = 1, message = "查询数量不能小于 0") 30 | private Integer pageSize; 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/article/ArticleRouteAndTitleVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.article; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.io.Serializable; 10 | 11 | /** 12 | * 文章标题和路由信息实体类 13 | * 14 | * @author hsuyeung 15 | * @date 2022/06/21 16 | */ 17 | @ApiModel(description = "文章标题和路由信息实体类") 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | @Data 21 | public class ArticleRouteAndTitleVO implements Serializable { 22 | private static final long serialVersionUID = -1623338134333686988L; 23 | 24 | @ApiModelProperty("文章标题") 25 | private String title; 26 | 27 | @ApiModelProperty("文章路由") 28 | private String route; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/permission/PermissionVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.permission; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * 权限信息 11 | * 12 | * @author hsuyeung 13 | * @date 2022/06/28 14 | */ 15 | @ApiModel(description = "权限信息") 16 | @Data 17 | public class PermissionVO implements Serializable { 18 | private static final long serialVersionUID = -925678122207529507L; 19 | 20 | @ApiModelProperty("接口路径") 21 | private String path; 22 | 23 | @ApiModelProperty("HTTP 方法类型") 24 | private String method; 25 | 26 | @ApiModelProperty("接口描述") 27 | private String permissionDesc; 28 | 29 | @ApiModelProperty("是否可用") 30 | private Boolean enabled; 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/file/FileSearchDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.file; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiParam; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.Size; 8 | import java.io.Serializable; 9 | 10 | /** 11 | * 文件分页搜索条件 12 | * 13 | * @author hsuyeung 14 | * @since 2023/8/26 15 | */ 16 | @Data 17 | @ApiModel(description = "文件分页搜索条件") 18 | public class FileSearchDTO implements Serializable { 19 | private static final long serialVersionUID = 1514124102384816338L; 20 | 21 | @ApiParam("文件 url") 22 | @Size(max = 255, message = "文件 url 不能超过 255 个字符") 23 | private String url; 24 | 25 | @ApiParam("开始时间戳") 26 | private Long startTimestamp; 27 | 28 | @ApiParam("结束时间戳") 29 | private Long endTimestamp; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/constant/DateFormatConstants.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.constant; 2 | 3 | /** 4 | * 时间格式常量 5 | * 6 | * @author hsuyeung 7 | * @date 2022/06/22 8 | */ 9 | public final class DateFormatConstants { 10 | public static final String FORMAT_YEAR_TO_SECOND = "yyyy-MM-dd HH:mm:ss"; 11 | public static final String FORMAT_YEAR_TO_DAY_SPLIT_BY_CN = "yyyy 年 MM 月 dd 日"; 12 | public static final String FORMAT_YEAR_TO_MONTH_SPLIT_BY_CN = "yyyy 年 MM 月"; 13 | public static final String FORMAT_YEAR_TO_DAY_SPLIT_BY_FORWARD_SLASH = "yyyy/MM/dd"; 14 | public static final String FORMAT_RFC_822 = "EEE, dd MMM yyyy HH:mm:ss z"; 15 | public static final String ISO_8601 = "yyyy-MM-dd'T'HH:mm:ss'Z'"; 16 | public static final String FORMAT_YMD_HM = "yyyy-MM-dd HH:mm"; 17 | 18 | private DateFormatConstants() { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/role/edit_role_panel.html: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/user/UserLoginRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.user; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import java.io.Serializable; 9 | 10 | /** 11 | * 用户登录请求参数 12 | * 13 | * @author hsuyeung 14 | * @date 2022/06/29 15 | */ 16 | @ApiModel(description = "用户登录请求参数") 17 | @Data 18 | public class UserLoginRequestDTO implements Serializable { 19 | private static final long serialVersionUID = -1504299503986897950L; 20 | 21 | @ApiModelProperty(value = "用户名", required = true) 22 | @NotBlank(message = "用户名不能为空") 23 | private String username; 24 | 25 | @ApiModelProperty(value = "用户密码", required = true) 26 | @NotBlank(message = "用户密码不能为空") 27 | private String password; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/user/UserSearchDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.user; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiParam; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.Size; 8 | import java.io.Serializable; 9 | 10 | /** 11 | * 用户分页搜索条件 12 | * 13 | * @author hsuyeung 14 | * @since 2023/8/26 15 | */ 16 | @Data 17 | @ApiModel(description = "用户分页搜索条件") 18 | public class UserSearchDTO implements Serializable { 19 | private static final long serialVersionUID = -6812909647978100695L; 20 | 21 | @ApiParam("用户名") 22 | @Size(max = 32, message = "用户名不能超过 32 个字符") 23 | private String username; 24 | 25 | @ApiParam("昵称") 26 | @Size(max = 64, message = "昵称不能超过 64 个字符") 27 | private String nickname; 28 | 29 | @ApiParam("是否可用") 30 | private Boolean enabled; 31 | } 32 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/user/assign_role_panel.html: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/PageSearchDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | import javax.validation.Valid; 11 | import javax.validation.constraints.NotNull; 12 | 13 | /** 14 | * 分页搜索参数 15 | * 16 | * @author hsuyeung 17 | * @date 2022/11/04 18 | */ 19 | @Data 20 | @Builder 21 | @NoArgsConstructor 22 | @AllArgsConstructor 23 | @ApiModel(description = "分页搜索参数") 24 | public class PageSearchDTO { 25 | 26 | @ApiModelProperty("查询参数实体") 27 | @Valid 28 | private T searchParam; 29 | 30 | @ApiModelProperty("分页参数实体") 31 | @NotNull(message = "pageParam 不能为 null") 32 | @Valid 33 | private PageDTO pageParam; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/config/ScheduleThreadPoolConfig.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.scheduling.annotation.EnableScheduling; 6 | import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; 7 | 8 | /** 9 | * 定时任务异步线程池配置 10 | * 11 | * @author hsuyeung 12 | * @date 2022/05/26 13 | */ 14 | @EnableScheduling 15 | @Configuration 16 | public class ScheduleThreadPoolConfig { 17 | 18 | @Bean 19 | public ThreadPoolTaskScheduler asyncTaskScheduler() { 20 | ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); 21 | scheduler.setPoolSize(4); 22 | scheduler.setThreadNamePrefix("async-task-scheduler-"); 23 | scheduler.initialize(); 24 | return scheduler; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/resources/static/css/about_footer.css: -------------------------------------------------------------------------------- 1 | .about { 2 | display: block; 3 | padding-top: 1.4rem; 4 | border-top: .1rem solid rgba(154, 128, 92, .2); 5 | margin-top: 1.6rem; 6 | max-width: 100%; 7 | margin-bottom: -3.2rem; 8 | font-size: 1.5rem 9 | } 10 | 11 | .about-avatar { 12 | float: left 13 | } 14 | 15 | .avatar { 16 | display: inline; 17 | width: 2.3rem; 18 | height: 2.3rem 19 | } 20 | 21 | .about-text { 22 | margin-left: 1rem; 23 | vertical-align: top 24 | } 25 | 26 | .about-nav { 27 | float: right 28 | } 29 | 30 | .about a:link, .about a:visited, .nav a:link, .nav a:visited { 31 | color: #503214 32 | } 33 | 34 | .about a:hover, .nav a:hover { 35 | color: rgb(235, 171, 10, 1) 36 | } 37 | 38 | .about [class*=" icon-"], .about [class^=icon-] { 39 | margin-right: .2rem 40 | } 41 | 42 | @media (max-device-width: 700px),(max-width: 700px) { 43 | .about { 44 | display: none 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/web/core/RequestUserHolder.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.web.core; 2 | 3 | /** 4 | * 全局保存当前操作用户信息 5 | * 6 | * @author hsuyeung 7 | * @date 2022/07/10 8 | */ 9 | public final class RequestUserHolder { 10 | /** 11 | * 保存用户 id的 ThreadLocal 12 | */ 13 | private static final ThreadLocal userThreadLocal = new ThreadLocal<>(); 14 | 15 | /** 16 | * 添加当前操作人 id 17 | */ 18 | public static void addCurrentUid(Long uid) { 19 | userThreadLocal.set(uid); 20 | } 21 | 22 | /** 23 | * 获取当前操作人的 id 24 | * 25 | * @return 当前操作人的 id 26 | */ 27 | public static Long getCurrentUid() { 28 | return userThreadLocal.get(); 29 | } 30 | 31 | 32 | /** 33 | * 防止内存泄漏 34 | */ 35 | public static void remove() { 36 | userThreadLocal.remove(); 37 | } 38 | 39 | 40 | private RequestUserHolder() { 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/entity/FileEntity.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.TableField; 4 | import com.baomidou.mybatisplus.annotation.TableName; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.NoArgsConstructor; 9 | import lombok.experimental.Accessors; 10 | import lombok.experimental.SuperBuilder; 11 | 12 | /** 13 | * 文件对象 14 | * 15 | * @author hsuyeung 16 | * @date 2022/06/27 17 | */ 18 | @Data 19 | @EqualsAndHashCode(callSuper = true) 20 | @SuperBuilder 21 | @NoArgsConstructor 22 | @AllArgsConstructor 23 | @Accessors(chain = true) 24 | @TableName("t_file") 25 | public class FileEntity extends BaseEntity { 26 | private static final long serialVersionUID = 9213071291143400976L; 27 | 28 | /** 29 | * 文件访问全路径 30 | */ 31 | @TableField("url") 32 | private String url; 33 | } 34 | -------------------------------------------------------------------------------- /src/main/resources/static/plugin/highlight/custom-style.css: -------------------------------------------------------------------------------- 1 | pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#fef9e7;color:#503214}.hljs-comment{color:#697070}.hljs-punctuation,.hljs-tag{color:rgba(68,68,68,.67)}.hljs-tag .hljs-attr,.hljs-tag .hljs-name{color:#444}.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-tag{font-weight:700}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#800}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-link,.hljs-operator,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#ab5656}.hljs-literal{color:#695}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta .hljs-string{color:#38a}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700} 2 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/util/FileUtil.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.util; 2 | 3 | /** 4 | * 文件相关工具类 5 | * 6 | * @author hsuyeung 7 | * @date 2022/06/27 8 | */ 9 | public final class FileUtil { 10 | /** 11 | * 获取文件名的后缀 12 | * 13 | * @param fileName 文件名 14 | * @return 后缀 15 | */ 16 | public static String getSuffix(String fileName) { 17 | AssertUtil.hasLength(fileName, "fileName 不能为空"); 18 | int lastIndexOfDot = fileName.lastIndexOf("."); 19 | if (lastIndexOfDot < 0) { 20 | return ""; 21 | } 22 | return fileName.substring(lastIndexOfDot + 1); 23 | } 24 | 25 | /** 26 | * 获取小写的文件名后缀 27 | * 28 | * @param fileName 文件名 29 | * @return 小写的文件名后缀 30 | */ 31 | public static String getSuffixLowercase(String fileName) { 32 | return getSuffix(fileName).toLowerCase(); 33 | } 34 | 35 | private FileUtil() { 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/role/assign_permission_panel.html: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | logging: 2 | config: classpath:logback-spring.xml 3 | spring: 4 | profiles: 5 | active: dev 6 | # mybatis plus 相关配置 7 | mybatis-plus: 8 | configuration: 9 | log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 10 | type-aliases-package: com.hsuyeung.blog.model.entity 11 | type-enums-package: com.hsuyeung.blog.constant.enums 12 | # mapper xml 映射文件所在位置 13 | mapper-locations: classpath:mapper/*Mapper.xml 14 | global-config: 15 | db-config: 16 | # 全局逻辑删除字段名 17 | logic-delete-field: deleted 18 | # 逻辑已删除值(默认为 1) 19 | logic-delete-value: 1 20 | # 逻辑未删除值(默认为 0) 21 | logic-not-delete-value: 0 22 | # 自定义 HttpClient 的 RequestConfig 配置 23 | http: 24 | client: 25 | request: 26 | config: 27 | connectionRequestTimeout: 6000 28 | connectTimeout: 6000 29 | socketTimeout: 6000 30 | server: 31 | compression: 32 | enabled: true 33 | min-response-size: 1024 34 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/article/ArticleDetailVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.article; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.Data; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * 文章详情 10 | * 11 | * @author hsuyeung 12 | * @date 2022/06/06 13 | */ 14 | @Data 15 | public class ArticleDetailVO implements Serializable { 16 | private static final long serialVersionUID = -7853559111414225454L; 17 | 18 | @ApiModelProperty("文章 id") 19 | private Long id; 20 | 21 | @ApiModelProperty("文章标题") 22 | private String title; 23 | 24 | @ApiModelProperty("文章作者") 25 | private String author; 26 | 27 | @ApiModelProperty("文章关键词") 28 | private String keywords; 29 | 30 | @ApiModelProperty("文章描述") 31 | private String description; 32 | 33 | @ApiModelProperty("文章内容") 34 | private String content; 35 | 36 | @ApiModelProperty("创建时间") 37 | private String createTime; 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/constant/enums/MailStatusEnum.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.constant.enums; 2 | 3 | import com.baomidou.mybatisplus.annotation.EnumValue; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | 7 | import java.util.Objects; 8 | 9 | /** 10 | * 邮件发送状态枚举 11 | * 12 | * @author hsuyeung 13 | * @date 2022/06/18 14 | */ 15 | @Getter 16 | @AllArgsConstructor 17 | public enum MailStatusEnum { 18 | /** 19 | * 发送失败 20 | */ 21 | FAILED(0, "发送失败"), 22 | 23 | /** 24 | * 发送成功 25 | */ 26 | SUCCESS(1, "发送成功"); 27 | 28 | @EnumValue 29 | private final Integer code; 30 | private final String desc; 31 | 32 | public static MailStatusEnum getByCode(Integer code) { 33 | for (MailStatusEnum statusEnum : MailStatusEnum.values()) { 34 | if (Objects.equals(statusEnum.getCode(), code)) { 35 | return statusEnum; 36 | } 37 | } 38 | return null; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/role/CreateRoleRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.role; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import java.io.Serializable; 10 | 11 | /** 12 | * 创建角色请求参数 13 | * 14 | * @author hsuyeung 15 | * @date 2022/06/29 16 | */ 17 | @ApiModel(description = "创建角色请求参数") 18 | @Data 19 | public class CreateRoleRequestDTO implements Serializable { 20 | private static final long serialVersionUID = -6899435706413071318L; 21 | 22 | @ApiModelProperty(value = "角色编码", required = true) 23 | @NotBlank(message = "角色编码不能为空") 24 | private String roleCode; 25 | 26 | @ApiModelProperty(value = "角色描述") 27 | private String roleDesc; 28 | 29 | @ApiModelProperty(value = "是否可用", required = true) 30 | @NotNull(message = "角色是否可用不能为空") 31 | private Boolean enabled; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/user/UserLoginDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.user; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.Size; 9 | import java.io.Serializable; 10 | 11 | /** 12 | * 用户登录请求参数 13 | * 14 | * @author hsuyeung 15 | * @date 2022/06/29 16 | */ 17 | @ApiModel(description = "用户登录请求参数") 18 | @Data 19 | public class UserLoginDTO implements Serializable { 20 | private static final long serialVersionUID = -1504299503986897950L; 21 | 22 | @ApiModelProperty(value = "用户名", required = true) 23 | @NotBlank(message = "用户名不能为空") 24 | @Size(max = 32, message = "用户名不能超过 32 个字符") 25 | private String username; 26 | 27 | @ApiModelProperty(value = "用户密码", required = true) 28 | @NotBlank(message = "用户密码不能为空") 29 | @Size(max = 16, message = "密码不能超过 16 个字符") 30 | private String password; 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/util/MD5Util.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.util; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | 5 | import java.io.UnsupportedEncodingException; 6 | import java.nio.charset.StandardCharsets; 7 | import java.security.MessageDigest; 8 | import java.security.NoSuchAlgorithmException; 9 | 10 | /** 11 | * @author hsuyeung 12 | * @date 2022/06/18 13 | */ 14 | @Slf4j 15 | public final class MD5Util { 16 | public static String hex(byte[] array) { 17 | StringBuilder sb = new StringBuilder(); 18 | for (byte b : array) { 19 | sb.append(Integer.toHexString((b & 0xFF) | 0x100), 1, 3); 20 | } 21 | return sb.toString(); 22 | } 23 | 24 | public static String md5Hex(String message) throws NoSuchAlgorithmException, UnsupportedEncodingException { 25 | MessageDigest md = MessageDigest.getInstance("MD5"); 26 | return hex(md.digest(message.getBytes(StandardCharsets.UTF_8))); 27 | } 28 | 29 | private MD5Util() { 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/exception/BizException.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.exception; 2 | 3 | import lombok.Data; 4 | import lombok.EqualsAndHashCode; 5 | 6 | import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; 7 | 8 | /** 9 | * 业务异常 10 | *

该异常是可以返回给客户端展示的信息

11 | * 12 | * @author hsuyeung 13 | * @date 2022/05/14 14 | */ 15 | @Data 16 | @EqualsAndHashCode(callSuper = true) 17 | public class BizException extends RuntimeException { 18 | private static final long serialVersionUID = -5040009887655650906L; 19 | 20 | private final Integer code; 21 | 22 | public BizException(String message) { 23 | this(INTERNAL_SERVER_ERROR.value(), message, null); 24 | } 25 | 26 | public BizException(String message, Throwable cause) { 27 | this(INTERNAL_SERVER_ERROR.value(), message, cause); 28 | } 29 | 30 | public BizException(Integer code, String message, Throwable cause) { 31 | super(message, cause); 32 | this.code = code; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/util/CommonUtil.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.util; 2 | 3 | import java.util.regex.Matcher; 4 | 5 | import static com.hsuyeung.blog.constant.RegexConstants.*; 6 | 7 | /** 8 | * @author hsuyeung 9 | * @date 2022/06/21 10 | */ 11 | public final class CommonUtil { 12 | /** 13 | * 清除所有的 html 标签 14 | * 15 | * @param htmlStr 带有 html 标签的字符串 16 | * @return 清除 html 标签后的字符串 17 | */ 18 | public static String removeTag(String htmlStr) { 19 | Matcher mScript = JAVASCRIPT_TAG_PATTERN.matcher(htmlStr); 20 | htmlStr = mScript.replaceAll(""); 21 | Matcher mStyle = STYLE_TAG_PATTERN.matcher(htmlStr); 22 | htmlStr = mStyle.replaceAll(""); 23 | Matcher mHtml = HTML_TAG_PATTERN.matcher(htmlStr); 24 | htmlStr = mHtml.replaceAll(""); 25 | Matcher mSpace = SPACE_TAG_PATTERN.matcher(htmlStr); 26 | htmlStr = mSpace.replaceAll(""); 27 | return htmlStr; 28 | } 29 | 30 | private CommonUtil() { 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/permission/add_permission_panel.html: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/cache/lfu/Node.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.cache.lfu; 2 | 3 | /** 4 | * 双层双向链表的内层双向链表中的单个结点的定义 5 | *

这一个结点即表示一个缓存数据

6 | * 7 | * @author hsuyeung 8 | * @date 2022/12/04 9 | */ 10 | class Node { 11 | /** 12 | * 缓存的 key 13 | */ 14 | String key; 15 | 16 | /** 17 | * 缓存的值 18 | */ 19 | byte[] value; 20 | 21 | /** 22 | * 该缓存的访问频次 23 | */ 24 | int freq; 25 | 26 | /** 27 | * 当前结点的前一个结点 28 | */ 29 | Node prevNode; 30 | 31 | /** 32 | * 当前结点的下一个结点 33 | */ 34 | Node nextNode; 35 | 36 | /** 37 | * 该结点所属的外层链表引用 38 | */ 39 | DoubleLinkedList doubleLinkedList; 40 | 41 | 42 | /** 43 | * 创建一个空的 Node 44 | */ 45 | Node() { 46 | } 47 | 48 | /** 49 | * 创建一个带缓存数据的 Node 50 | * 51 | * @param key 缓存的 key 52 | * @param value 缓存的 value 53 | */ 54 | Node(String key, byte[] value) { 55 | this.key = key; 56 | this.value = value; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/permission/PermissionSearchDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.permission; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiParam; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.Size; 8 | import java.io.Serializable; 9 | 10 | /** 11 | * 权限分页搜索条件 12 | * 13 | * @author hsuyeung 14 | * @since 2023/8/26 15 | */ 16 | @Data 17 | @ApiModel(description = "权限分页搜索条件") 18 | public class PermissionSearchDTO implements Serializable { 19 | private static final long serialVersionUID = -7274276198414622500L; 20 | 21 | @ApiParam("权限路径") 22 | @Size(max = 255, message = "接口路径不能超过 255 个字符") 23 | private String path; 24 | 25 | @ApiParam("请求方法类型") 26 | @Size(max = 32, message = "HTTP 方法类型不能超过 32 个字符") 27 | private String method; 28 | 29 | @ApiParam("权限描述") 30 | @Size(max = 255, message = "接口描述不能超过 255 个字符") 31 | private String permissionDesc; 32 | 33 | @ApiParam("是否可用") 34 | private Boolean enabled; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/entity/UserRoleEntity.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.TableField; 4 | import com.baomidou.mybatisplus.annotation.TableName; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.NoArgsConstructor; 9 | import lombok.experimental.Accessors; 10 | import lombok.experimental.SuperBuilder; 11 | 12 | /** 13 | * 用户-角色关系 14 | * 15 | * @author hsuyeung 16 | * @date 2022/06/28 17 | */ 18 | @Data 19 | @EqualsAndHashCode(callSuper = true) 20 | @SuperBuilder 21 | @NoArgsConstructor 22 | @AllArgsConstructor 23 | @Accessors(chain = true) 24 | @TableName("t_user_role") 25 | public class UserRoleEntity extends BaseEntity { 26 | private static final long serialVersionUID = -1603387007204442533L; 27 | 28 | /** 29 | * 用户 id 30 | */ 31 | @TableField("uid") 32 | private Long uid; 33 | 34 | /** 35 | * 角色 id 36 | */ 37 | @TableField("rid") 38 | private Long rid; 39 | } 40 | -------------------------------------------------------------------------------- /src/main/resources/static/css/iconmoon.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'icomoon'; 3 | src: url('../fonts/iconmoon/icomoon.eot?k18w5r'); 4 | src: url('../fonts/iconmoon/icomoon.eot?k18w5r#iefix') format('embedded-opentype'), 5 | url('../fonts/iconmoon/icomoon.ttf?k18w5r') format('truetype'), 6 | url('../fonts/iconmoon/icomoon.woff?k18w5r') format('woff'), 7 | url('../fonts/iconmoon/icomoon.svg?k18w5r#icomoon') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | font-display: block; 11 | } 12 | 13 | [class^="icon-"], [class*=" icon-"] { 14 | /* use !important to prevent issues with browser extensions that change fonts */ 15 | font-family: 'icomoon' !important; 16 | speak: never; 17 | font-style: normal; 18 | font-weight: normal; 19 | font-variant: normal; 20 | text-transform: none; 21 | line-height: 1; 22 | 23 | /* Better Font Rendering =========== */ 24 | -webkit-font-smoothing: antialiased; 25 | -moz-osx-font-smoothing: grayscale; 26 | } 27 | 28 | .icon-mail:before { 29 | content: "\e900"; 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/systemconfig/SystemConfigSearchDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.systemconfig; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiParam; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.Size; 8 | import java.io.Serializable; 9 | 10 | /** 11 | * 系统配置分页搜索条件 12 | * 13 | * @author hsuyeung 14 | * @since 2023/8/26 15 | */ 16 | @Data 17 | @ApiModel(description = "系统配置分页搜索条件") 18 | public class SystemConfigSearchDTO implements Serializable { 19 | private static final long serialVersionUID = -1445347751276312277L; 20 | 21 | @ApiParam("系统配置 key") 22 | @Size(max = 64, message = "系统配置 key 不能超过 64 个字符") 23 | private String key; 24 | 25 | @ApiParam("系统配置分组") 26 | @Size(max = 255, message = "系统配置分组不能超过 255 个字符") 27 | private String group; 28 | 29 | @ApiParam("系统配置描述") 30 | @Size(max = 255, message = "系统配置描述不能超过 255 个字符") 31 | private String desc; 32 | 33 | @ApiParam("是否可用") 34 | private Boolean enabled; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/constant/enums/MailTypeEnum.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.constant.enums; 2 | 3 | import com.baomidou.mybatisplus.annotation.EnumValue; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | 7 | import java.util.Objects; 8 | 9 | /** 10 | * 邮件类型枚举 11 | * 12 | * @author hsuyeung 13 | * @date 2022/06/18 14 | */ 15 | @Getter 16 | @AllArgsConstructor 17 | public enum MailTypeEnum { 18 | /** 19 | * 未知类型 20 | */ 21 | UNKNOWN(0, "unknown"), 22 | /** 23 | * 评论回复提醒 24 | */ 25 | COMMENT_BE_REPLIED(1, "评论被回复"), 26 | /** 27 | * 文章/留言板收到评论 28 | */ 29 | BE_COMMENTED(2, "文章/留言板收到评论"); 30 | 31 | @EnumValue 32 | private final Integer code; 33 | private final String desc; 34 | 35 | public static MailTypeEnum getByCode(Integer code) { 36 | for (MailTypeEnum typeEnum : MailTypeEnum.values()) { 37 | if (Objects.equals(typeEnum.getCode(), code)) { 38 | return typeEnum; 39 | } 40 | } 41 | return null; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/entity/RolePermissionEntity.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.TableField; 4 | import com.baomidou.mybatisplus.annotation.TableName; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.NoArgsConstructor; 9 | import lombok.experimental.Accessors; 10 | import lombok.experimental.SuperBuilder; 11 | 12 | /** 13 | * 角色-权限关系 14 | * 15 | * @author hsuyeung 16 | * @date 2022/06/28 17 | */ 18 | @Data 19 | @EqualsAndHashCode(callSuper = true) 20 | @SuperBuilder 21 | @NoArgsConstructor 22 | @AllArgsConstructor 23 | @Accessors(chain = true) 24 | @TableName("t_role_permission") 25 | public class RolePermissionEntity extends BaseEntity { 26 | private static final long serialVersionUID = 3725411417828926291L; 27 | 28 | /** 29 | * 角色 id 30 | */ 31 | @TableField("rid") 32 | private Long rid; 33 | 34 | /** 35 | * 权限 id 36 | */ 37 | @TableField("pid") 38 | private Long pid; 39 | } 40 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/user/add_user_panel.html: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/constant/enums/CommentNotificationSwitchEnum.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.constant.enums; 2 | 3 | import com.baomidou.mybatisplus.annotation.EnumValue; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | 7 | import java.util.Objects; 8 | 9 | /** 10 | * 评论回复提醒开关枚举 11 | * 12 | * @author hsuyeung 13 | * @date 2022/06/17 14 | */ 15 | @Getter 16 | @AllArgsConstructor 17 | public enum CommentNotificationSwitchEnum { 18 | /** 19 | * 不提醒 20 | */ 21 | OFF(0, "不提醒"), 22 | 23 | /** 24 | * 提醒 25 | */ 26 | ON(1, "提醒"); 27 | 28 | @EnumValue 29 | private final Integer code; 30 | private final String desc; 31 | 32 | public static CommentNotificationSwitchEnum getByCode(Integer code) { 33 | for (CommentNotificationSwitchEnum notificationSwitchEnum : CommentNotificationSwitchEnum.values()) { 34 | if (Objects.equals(notificationSwitchEnum.getCode(), code)) { 35 | return notificationSwitchEnum; 36 | } 37 | } 38 | return null; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/comment/CommentItemVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.comment; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.Data; 5 | 6 | import java.io.Serializable; 7 | import java.util.List; 8 | 9 | /** 10 | * 页面评论信息实体类 11 | * 12 | * @author hsuyeung 13 | * @date 2022/06/13 14 | */ 15 | @Data 16 | public class CommentItemVO implements Serializable, Comparable { 17 | private static final long serialVersionUID = -2145077569133320449L; 18 | 19 | @ApiModelProperty("评论 id") 20 | private Long id; 21 | 22 | @ApiModelProperty("评论内容") 23 | private String content; 24 | 25 | @ApiModelProperty("时间信息") 26 | private CommentTimeVO time; 27 | 28 | @ApiModelProperty("用户信息") 29 | private CommentUserVO user; 30 | 31 | @ApiModelProperty("二级评论列表") 32 | private List childComments; 33 | 34 | @Override 35 | public int compareTo(CommentItemVO o) { 36 | return this.getTime().getFormattedTime().compareTo(o.getTime().getFormattedTime()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/friendlink/FriendLinkSearchDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.friendlink; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiParam; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.Size; 8 | import java.io.Serializable; 9 | 10 | /** 11 | * 友链分页搜索条件 12 | * 13 | * @author hsuyeung 14 | * @since 2023/8/26 15 | */ 16 | @Data 17 | @ApiModel(description = "友链分页搜索条件") 18 | public class FriendLinkSearchDTO implements Serializable { 19 | private static final long serialVersionUID = 6880119853911353402L; 20 | 21 | @ApiParam("友链名字") 22 | @Size(max = 255, message = "友链名称不能超过 255 个字符") 23 | private String linkName; 24 | 25 | @ApiParam("友链链接") 26 | @Size(max = 255, message = "友链链接不能超过 255 个字符") 27 | private String linkUrl; 28 | 29 | @ApiParam("友链描述") 30 | @Size(max = 255, message = "友链描述不能超过 255 个字符") 31 | private String linkDesc; 32 | 33 | @ApiParam("友链分组") 34 | @Size(max = 255, message = "友链分组不能超过 255 个字符") 35 | private String linkGroup; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/user/UserInfoVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.user; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * @author hsuyeung 11 | * @date 2022/07/01 12 | */ 13 | @ApiModel(description = "分页列表用户信息") 14 | @Data 15 | public class UserInfoVO implements Serializable { 16 | private static final long serialVersionUID = -7564794978001708513L; 17 | 18 | @ApiModelProperty("用户 id") 19 | private Long id; 20 | 21 | @ApiModelProperty("用户名") 22 | private String username; 23 | 24 | @ApiModelProperty("昵称") 25 | private String nickname; 26 | 27 | @ApiModelProperty("账户是否可用") 28 | private Boolean enabled; 29 | 30 | @ApiModelProperty("创建时间") 31 | private String createTime; 32 | 33 | @ApiModelProperty("创建人") 34 | private String createBy; 35 | 36 | @ApiModelProperty("更新时间") 37 | private String updateTime; 38 | 39 | @ApiModelProperty("更新人") 40 | private String updateBy; 41 | } 42 | -------------------------------------------------------------------------------- /src/main/resources/static/plugin/message/message.min.js: -------------------------------------------------------------------------------- 1 | class Message{constructor(){const containerId='message-container';this.containerEl=document.getElementById(containerId);if(!this.containerEl){this.containerEl=document.createElement('div');this.containerEl.id=containerId;document.body.appendChild(this.containerEl)}}show({type='info',text='',duration=2000,closeable=false}){let messageEl=document.createElement('div');messageEl.className='message move-in';messageEl.innerHTML=`
${text}
`;if(closeable){let closeEl=document.createElement('div');closeEl.className='close icon icon-close';messageEl.appendChild(closeEl);closeEl.addEventListener('click',()=>{this.close(messageEl)})}this.containerEl.appendChild(messageEl);if(duration>0){setTimeout(()=>{this.close(messageEl)},duration)}}close(messageEl){messageEl.className=messageEl.className.replace('move-in','');messageEl.className+='move-out';messageEl.addEventListener('animationend',()=>{messageEl.setAttribute('style','height: 0; margin: 0')});messageEl.addEventListener('transitionend',()=>{messageEl.remove()})}} 2 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/service/IRbacAuthorityService.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.service; 2 | 3 | import org.springframework.validation.annotation.Validated; 4 | 5 | import javax.servlet.http.HttpServletRequest; 6 | import javax.validation.constraints.NotBlank; 7 | import javax.validation.constraints.NotNull; 8 | 9 | /** 10 | * RBAC 认证服务接口 11 | * 12 | * @author hsuyeung 13 | * @date 2022/06/28 14 | */ 15 | @Validated 16 | public interface IRbacAuthorityService { 17 | /** 18 | * 判断当前请求的用户是否有权限 19 | * 20 | * @param request {@link HttpServletRequest} 21 | * @return 有权限返回 true,无权限返回 false 22 | */ 23 | boolean hasPermission(@NotNull(message = "request 不能为 null") HttpServletRequest request); 24 | 25 | /** 26 | * 根据 token 判断是否有权限 27 | * 28 | * @param request {@link HttpServletRequest} 29 | * @param token token 30 | * @return 有权限返回 true,无权限返回 false 31 | */ 32 | boolean hasPermission(@NotNull(message = "request 不能为 null") HttpServletRequest request, 33 | @NotBlank(message = "token 不能为空") String token); 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/systemconfig/UpdateSystemConfigRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.systemconfig; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import java.io.Serializable; 10 | 11 | /** 12 | * 更新系统配置请求参数 13 | * 14 | * @author hsuyeung 15 | * @date 2022/07/06 16 | */ 17 | @ApiModel(description = "更新系统配置请求参数") 18 | @Data 19 | public class UpdateSystemConfigRequestDTO implements Serializable { 20 | private static final long serialVersionUID = -7576156634860516726L; 21 | 22 | @ApiModelProperty(value = "配置 id", required = true) 23 | @NotNull(message = "id 不能为空") 24 | private Long id; 25 | 26 | @ApiModelProperty(value = "配置 value", required = true) 27 | @NotBlank(message = "confValue 不能为空") 28 | private String confValue; 29 | 30 | @ApiModelProperty(value = "配置是否可用", required = true) 31 | @NotNull(message = "enabled 不能为 null") 32 | private Boolean enabled; 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/entity/ContentEntity.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.TableField; 4 | import com.baomidou.mybatisplus.annotation.TableName; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.NoArgsConstructor; 9 | import lombok.experimental.Accessors; 10 | import lombok.experimental.SuperBuilder; 11 | 12 | /** 13 | *

14 | * 文章内容表 15 | *

16 | * 17 | * @author hsuyeung 18 | * @since 2022/06/05 19 | */ 20 | @Data 21 | @EqualsAndHashCode(callSuper = true) 22 | @SuperBuilder 23 | @NoArgsConstructor 24 | @AllArgsConstructor 25 | @Accessors(chain = true) 26 | @TableName("t_content") 27 | public class ContentEntity extends BaseEntity { 28 | private static final long serialVersionUID = -8940387629564988992L; 29 | 30 | /** 31 | * markdown 格式文章内容 32 | */ 33 | @TableField("md_content") 34 | private String mdContent; 35 | 36 | /** 37 | * html 格式文章内容 38 | */ 39 | @TableField("html_content") 40 | private String htmlContent; 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/sitemap/URLNode.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.sitemap; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.io.Serializable; 9 | import java.time.LocalDateTime; 10 | 11 | /** 12 | * sitemap 的 url 节点定义 13 | *

14 | * XML站点地图的格式规范 15 | * 16 | * @author hsuyeung 17 | * @date 2023/11/10 18 | */ 19 | @Data 20 | @Builder 21 | @NoArgsConstructor 22 | @AllArgsConstructor 23 | public class URLNode implements Serializable { 24 | private static final long serialVersionUID = -3920031698141754541L; 25 | /** 26 | * 具体链接 27 | */ 28 | private String loc; 29 | 30 | /** 31 | * 最后一次更新时间 32 | */ 33 | private LocalDateTime lastMod; 34 | 35 | /** 36 | * 抓取频率 37 | * 38 | * @see ChangeFreqEnum 39 | */ 40 | private ChangeFreqEnum changeFreq; 41 | 42 | /** 43 | * 权重 44 | *

45 | * [0.0-1.0] 之间,默认 0.5 46 | */ 47 | private double priority = 0.5; 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/role/RoleInfoVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.role; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * 分页列表角色信息 11 | * 12 | * @author hsuyeung 13 | * @date 2022/07/03 14 | */ 15 | @ApiModel(description = "分页列表角色信息") 16 | @Data 17 | public class RoleInfoVO implements Serializable { 18 | private static final long serialVersionUID = -1452250785010877478L; 19 | 20 | @ApiModelProperty("角色 id") 21 | private Long id; 22 | 23 | @ApiModelProperty("角色编码") 24 | private String roleCode; 25 | 26 | @ApiModelProperty("角色描述") 27 | private String roleDesc; 28 | 29 | @ApiModelProperty("角色是否可用") 30 | private Boolean enabled; 31 | 32 | @ApiModelProperty("创建时间") 33 | private String createTime; 34 | 35 | @ApiModelProperty("创建人") 36 | private String createBy; 37 | 38 | @ApiModelProperty("更新时间") 39 | private String updateTime; 40 | 41 | @ApiModelProperty("更新人") 42 | private String updateBy; 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/exception/GlobalControllerExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.exception; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.web.bind.annotation.ExceptionHandler; 5 | import org.springframework.web.bind.annotation.RestControllerAdvice; 6 | import org.springframework.web.servlet.ModelAndView; 7 | 8 | /** 9 | * 全局 Controller 异常处理器 10 | * 11 | * @author hsuyeung 12 | * @date 2022/06/05 13 | */ 14 | @Slf4j 15 | @RestControllerAdvice("com.hsuyeung.blog.web.controller") 16 | public class GlobalControllerExceptionHandler { 17 | /** 18 | * 处理系统内部异常 19 | */ 20 | @ExceptionHandler(NotFoundException.class) 21 | public ModelAndView processNotFoundException(NotFoundException e) { 22 | log.error(e.getLocalizedMessage(), e); 23 | return new ModelAndView("error/404"); 24 | } 25 | 26 | /** 27 | * 其他异常 28 | */ 29 | @ExceptionHandler(Exception.class) 30 | public ModelAndView processException(Exception e) { 31 | log.error(e.getLocalizedMessage(), e); 32 | return new ModelAndView("error/500"); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/role/UpdateRoleRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.role; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import java.io.Serializable; 10 | 11 | /** 12 | * 更新角色请求参数 13 | * 14 | * @author hsuyeung 15 | * @date 2022/06/30 16 | */ 17 | @ApiModel(description = "更新角色请求参数") 18 | @Data 19 | public class UpdateRoleRequestDTO implements Serializable { 20 | private static final long serialVersionUID = -1330380436854897414L; 21 | 22 | @ApiModelProperty(value = "id", required = true) 23 | @NotNull(message = "id 不能为空") 24 | private Long id; 25 | 26 | @ApiModelProperty(value = "角色编码", required = true) 27 | @NotBlank(message = "角色编码不能为空") 28 | private String roleCode; 29 | 30 | @ApiModelProperty("角色描述") 31 | private String roleDesc; 32 | 33 | @ApiModelProperty(value = "角色是否可用", required = true) 34 | @NotNull(message = "角色是否可用不能为空") 35 | private Boolean enabled; 36 | } 37 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/permission/edit_permission_panel.html: -------------------------------------------------------------------------------- 1 |

27 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/role/CreateRoleDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.role; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import javax.validation.constraints.Size; 10 | import java.io.Serializable; 11 | 12 | /** 13 | * 创建角色请求参数 14 | * 15 | * @author hsuyeung 16 | * @date 2022/06/29 17 | */ 18 | @ApiModel(description = "创建角色请求参数") 19 | @Data 20 | public class CreateRoleDTO implements Serializable { 21 | private static final long serialVersionUID = -6899435706413071318L; 22 | 23 | @ApiModelProperty(value = "角色编码", required = true) 24 | @NotBlank(message = "角色编码不能为空") 25 | @Size(max = 64, message = "角色编码不能超过 64 个字符") 26 | private String roleCode; 27 | 28 | @ApiModelProperty(value = "角色描述") 29 | @Size(max = 255, message = "角色描述不能超过 255 个字符") 30 | private String roleDesc; 31 | 32 | @ApiModelProperty(value = "是否可用", required = true) 33 | @NotNull(message = "角色是否可用不能为 null") 34 | private Boolean enabled; 35 | } 36 | -------------------------------------------------------------------------------- /src/main/resources/static/css/friend_link_list.min.css: -------------------------------------------------------------------------------- 1 | .link-list{overflow:auto;padding:1rem 1rem 0;text-align:center}.link-list>.link-list-item{position:relative;float:left;overflow:hidden;margin:1.5rem .3rem;width:calc(33.33333% - 1.5rem);height:9rem;line-height:1.7rem;border:.1rem solid rgba(0,0,0,.1);border-radius:1rem}.link-list>.link-list-item:hover{background:#f4d03f}.link-list>.link-list-item:before{position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1;content:""}.link-list>.link-list-item a{color:#503214;text-decoration:none}.link-list>.link-list-item a .link-item-icon{float:left;overflow:hidden;margin:1.5rem 1rem;width:6rem;height:6rem;border-radius:3.5rem}.link-list>.link-list-item a .link-item-icon img{width:100%;height:100%;object-fit:cover}.link-item-name{padding:1.6rem 1rem 0 0;height:4rem;font-weight:700}.link-item-desc{height:5rem;font-size:1.5rem}.link-item-desc,.link-item-name{overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;white-space:nowrap}@media screen and (max-width:1024px){.link-list>.link-list-item{width:calc(50% - 1.5rem)!important}}@media screen and (max-width:600px){.link-list>.link-list-item{width:calc(100% - 1.5rem)!important}} 2 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/footer.html: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/annotation/ApiRateLimit.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.annotation; 2 | 3 | import com.hsuyeung.blog.constant.enums.ApiRateLimitStrategyEnum; 4 | 5 | import java.lang.annotation.Documented; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.Target; 8 | import java.time.temporal.ChronoUnit; 9 | 10 | import static com.hsuyeung.blog.constant.enums.ApiRateLimitStrategyEnum.IP; 11 | import static java.lang.annotation.ElementType.METHOD; 12 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 13 | 14 | 15 | /** 16 | * 接口请求速率限制 17 | * 18 | * @author hsuyeung 19 | * @date 2022/06/17 20 | */ 21 | @Target(METHOD) 22 | @Retention(RUNTIME) 23 | @Documented 24 | public @interface ApiRateLimit { 25 | /** 26 | * 两次请求间隔的时间,0 表示不限制 27 | */ 28 | long value() default 0; 29 | 30 | /** 31 | * 间隔时间的单位,默认为秒 32 | */ 33 | ChronoUnit timeUnit() default ChronoUnit.SECONDS; 34 | 35 | /** 36 | * 默认根据请求的 IP 地址进行限流 37 | */ 38 | ApiRateLimitStrategyEnum strategy() default IP; 39 | 40 | /** 41 | * 请求超过速率限制时的提示信息 42 | */ 43 | String message() default ""; 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/constant/enums/LogicDeleteEnum.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.constant.enums; 2 | 3 | import com.baomidou.mybatisplus.annotation.EnumValue; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | 7 | import java.util.Objects; 8 | 9 | /** 10 | * 逻辑删除枚举 11 | * 12 | * @author hsuyeung 13 | * @date 2022/02/21 14 | */ 15 | @Getter 16 | @AllArgsConstructor 17 | public enum LogicDeleteEnum { 18 | 19 | /** 20 | * 未删除 21 | */ 22 | NOT_DELETED(0, "未删除"), 23 | 24 | /** 25 | * 已删除 26 | */ 27 | DELETED(1, "已删除"); 28 | 29 | @EnumValue 30 | private final Integer code; 31 | private final String desc; 32 | 33 | /** 34 | * 根据 code 获取枚举 35 | * 36 | * @param code code 37 | * @return 如果存在该 code 的枚举值则返回该对象,否则返回 null 38 | */ 39 | public static LogicDeleteEnum getByCode(Integer code) { 40 | for (LogicDeleteEnum logicDeleteEnum : LogicDeleteEnum.values()) { 41 | if (Objects.equals(code, logicDeleteEnum.getCode())) { 42 | return logicDeleteEnum; 43 | } 44 | } 45 | return null; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/friend_link/add_friend_link_panel.html: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/systemconfig/UpdateSystemConfigDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.systemconfig; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import javax.validation.constraints.Size; 10 | import java.io.Serializable; 11 | 12 | /** 13 | * 更新系统配置请求参数 14 | * 15 | * @author hsuyeung 16 | * @date 2022/07/06 17 | */ 18 | @ApiModel(description = "更新系统配置请求参数") 19 | @Data 20 | public class UpdateSystemConfigDTO implements Serializable { 21 | private static final long serialVersionUID = -7576156634860516726L; 22 | 23 | @ApiModelProperty(value = "配置 id", required = true) 24 | @NotNull(message = "id 不能为 null") 25 | private Long id; 26 | 27 | @ApiModelProperty(value = "配置 value", required = true) 28 | @NotBlank(message = "confValue 不能为空") 29 | @Size(max = 4096, message = "系统配置 key 不能超过 4096 个字符") 30 | private String confValue; 31 | 32 | @ApiModelProperty(value = "配置是否可用", required = true) 33 | @NotNull(message = "enabled 不能为 null") 34 | private Boolean enabled; 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/permission/PermissionInfoVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.permission; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * 分页列表权限信息 11 | * 12 | * @author hsuyeung 13 | * @date 2022/07/04 14 | */ 15 | @ApiModel(description = "分页列表权限信息") 16 | @Data 17 | public class PermissionInfoVO implements Serializable { 18 | private static final long serialVersionUID = -1532842762404883238L; 19 | 20 | @ApiModelProperty("权限 id") 21 | private Long id; 22 | 23 | @ApiModelProperty("接口路径") 24 | private String path; 25 | 26 | @ApiModelProperty("HTTP 方法类型") 27 | private String method; 28 | 29 | @ApiModelProperty("接口描述") 30 | private String permissionDesc; 31 | 32 | @ApiModelProperty("是否可用") 33 | private Boolean enabled; 34 | 35 | @ApiModelProperty("创建时间") 36 | private String createTime; 37 | 38 | @ApiModelProperty("创建人") 39 | private String createBy; 40 | 41 | @ApiModelProperty("更新时间") 42 | private String updateTime; 43 | 44 | @ApiModelProperty("更新人") 45 | private String updateBy; 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/web/api/UserTokenApi.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.web.api; 2 | 3 | import com.hsuyeung.blog.service.IUserTokenService; 4 | import com.hsuyeung.blog.web.core.IBaseWebResponse; 5 | import com.hsuyeung.blog.web.core.WebResponse; 6 | import io.swagger.annotations.Api; 7 | import io.swagger.annotations.ApiOperation; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | /** 15 | * 用户 token 相关接口 16 | * 17 | * @author hsuyeung 18 | * @date 2022/07/01 19 | */ 20 | @Api(tags = "用户 token 相关接口") 21 | @RestController 22 | @RequestMapping("/api/user/token") 23 | @RequiredArgsConstructor 24 | public class UserTokenApi implements IBaseWebResponse { 25 | private final IUserTokenService userTokenService; 26 | 27 | @ApiOperation("校验 token 是否有效") 28 | @GetMapping("/validation") 29 | public WebResponse checkUserTokenValidation(@RequestParam String token) { 30 | return ok(!userTokenService.isExpired(token)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/permission/CreatePermissionRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.permission; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import java.io.Serializable; 10 | 11 | /** 12 | * 创建权限请求参数 13 | * 14 | * @author hsuyeung 15 | * @date 2022/06/30 16 | */ 17 | @ApiModel(description = "创建权限请求参数") 18 | @Data 19 | public class CreatePermissionRequestDTO implements Serializable { 20 | private static final long serialVersionUID = 3971000705974467539L; 21 | 22 | @ApiModelProperty(value = "接口路径", required = true) 23 | @NotBlank(message = "接口不能为空") 24 | private String path; 25 | 26 | @ApiModelProperty(value = "HTTP 方法类型", required = true) 27 | @NotBlank(message = "HTTP 方法类型不能为空") 28 | private String method; 29 | 30 | @ApiModelProperty(value = "接口描述", required = true) 31 | @NotBlank(message = "接口描述不能为空") 32 | private String permissionDesc; 33 | 34 | @ApiModelProperty(value = "是否可用", required = true) 35 | @NotNull(message = "是否可用不能为空") 36 | private Boolean enabled; 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/entity/RoleEntity.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.TableField; 4 | import com.baomidou.mybatisplus.annotation.TableName; 5 | import com.hsuyeung.blog.constant.enums.LogicSwitchEnum; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.NoArgsConstructor; 10 | import lombok.experimental.Accessors; 11 | import lombok.experimental.SuperBuilder; 12 | 13 | /** 14 | * 后台角色 15 | * 16 | * @author hsuyeung 17 | * @date 2022/06/28 18 | */ 19 | @Data 20 | @EqualsAndHashCode(callSuper = true) 21 | @SuperBuilder 22 | @NoArgsConstructor 23 | @AllArgsConstructor 24 | @Accessors(chain = true) 25 | @TableName("t_role") 26 | public class RoleEntity extends BaseEntity { 27 | private static final long serialVersionUID = -2310860896372658100L; 28 | 29 | /** 30 | * 角色编码 31 | */ 32 | @TableField("role_code") 33 | private String roleCode; 34 | 35 | /** 36 | * 角色描述 37 | */ 38 | @TableField("role_desc") 39 | private String roleDesc; 40 | 41 | /** 42 | * 是否可用 43 | */ 44 | @TableField("is_enabled") 45 | private LogicSwitchEnum enabled; 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/article/ArticleSearchDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.article; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiParam; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.Size; 8 | import java.io.Serializable; 9 | 10 | /** 11 | * 文章搜索条件 12 | * 13 | * @author hsuyeung 14 | * @since 2023/8/25 23:46 15 | */ 16 | @Data 17 | @ApiModel(description = "文章搜索条件") 18 | public class ArticleSearchDTO implements Serializable { 19 | private static final long serialVersionUID = 2880977410290356980L; 20 | 21 | @ApiParam("标题") 22 | @Size(max = 64, message = "文章标题不能超过 64 个字符") 23 | private String title; 24 | 25 | @ApiParam("作者") 26 | @Size(max = 20, message = "文章作者不能超过 20 个字符") 27 | private String author; 28 | 29 | @ApiParam("关键词") 30 | @Size(max = 255, message = "文章关键词不能超过 255 个字符") 31 | private String keywords; 32 | 33 | @ApiParam("描述") 34 | @Size(max = 255, message = "文章描述不能超过 255 个字符") 35 | private String desc; 36 | 37 | @ApiParam("是否置顶") 38 | private Boolean pin; 39 | 40 | @ApiParam("开始时间戳") 41 | private Long startTimestamp; 42 | 43 | @ApiParam("结束时间戳") 44 | private Long endTimestamp; 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/role/UpdateRoleDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.role; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import javax.validation.constraints.Size; 10 | import java.io.Serializable; 11 | 12 | /** 13 | * 更新角色请求参数 14 | * 15 | * @author hsuyeung 16 | * @date 2022/06/30 17 | */ 18 | @ApiModel(description = "更新角色请求参数") 19 | @Data 20 | public class UpdateRoleDTO implements Serializable { 21 | private static final long serialVersionUID = -1330380436854897414L; 22 | 23 | @ApiModelProperty(value = "id", required = true) 24 | @NotNull(message = "id 不能为 null") 25 | private Long id; 26 | 27 | @ApiModelProperty(value = "角色编码", required = true) 28 | @NotBlank(message = "角色编码不能为空") 29 | @Size(max = 64, message = "角色编码不能超过 64 个字符") 30 | private String roleCode; 31 | 32 | @ApiModelProperty("角色描述") 33 | @Size(max = 255, message = "角色描述不能超过 255 个字符") 34 | private String roleDesc; 35 | 36 | @ApiModelProperty(value = "角色是否可用", required = true) 37 | @NotNull(message = "角色是否可用不能为 null") 38 | private Boolean enabled; 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/friendlink/FriendLinkInfoVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.friendlink; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * 分页列表友链信息 11 | * 12 | * @author hsuyeung 13 | * @date 2022/07/05 14 | */ 15 | @ApiModel(description = "分页列表友链信息") 16 | @Data 17 | public class FriendLinkInfoVO implements Serializable { 18 | private static final long serialVersionUID = -3928047852266031557L; 19 | 20 | @ApiModelProperty("友链 id") 21 | private Long id; 22 | 23 | @ApiModelProperty("友链名称") 24 | private String linkName; 25 | 26 | @ApiModelProperty("友链链接") 27 | private String linkUrl; 28 | 29 | @ApiModelProperty("友链头像") 30 | private String linkAvatar; 31 | 32 | @ApiModelProperty("一句话描述") 33 | private String linkDesc; 34 | 35 | @ApiModelProperty("友链分组") 36 | private String linkGroup; 37 | 38 | @ApiModelProperty("创建时间") 39 | private String createTime; 40 | 41 | @ApiModelProperty("创建人") 42 | private String createBy; 43 | 44 | @ApiModelProperty("更新时间") 45 | private String updateTime; 46 | 47 | @ApiModelProperty("更新人") 48 | private String updateBy; 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/httpclient/HttpClientResult.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.httpclient; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.Data; 5 | import org.springframework.http.HttpStatus; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * 封装 httpClient 响应结果 11 | * 12 | * @author hsuyeung 13 | * @date 2021/10/24 14 | */ 15 | @JsonInclude(JsonInclude.Include.NON_NULL) 16 | @Data 17 | public class HttpClientResult implements Serializable { 18 | private static final long serialVersionUID = -5429992126452039696L; 19 | 20 | /** 21 | * 响应状态 22 | */ 23 | private HttpStatus status; 24 | 25 | /** 26 | * 响应数据 27 | */ 28 | private String content; 29 | 30 | public HttpClientResult() { 31 | } 32 | 33 | public HttpClientResult(int statusCode) { 34 | this(statusCode, null); 35 | } 36 | 37 | public HttpClientResult(HttpStatus status) { 38 | this(status, null); 39 | } 40 | 41 | public HttpClientResult(int statusCode, String content) { 42 | this(HttpStatus.resolve(statusCode), content); 43 | } 44 | 45 | public HttpClientResult(HttpStatus status, String content) { 46 | this.status = status; 47 | this.content = content; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/system_config/edit_system_config_panel.html: -------------------------------------------------------------------------------- 1 | 36 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/friend_link/edit_friend_link_panel.html: -------------------------------------------------------------------------------- 1 | 31 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/util/JwtUtil.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.util; 2 | 3 | import com.auth0.jwt.JWT; 4 | import com.auth0.jwt.JWTVerifier; 5 | import com.auth0.jwt.algorithms.Algorithm; 6 | import com.auth0.jwt.interfaces.Claim; 7 | 8 | import java.util.Map; 9 | 10 | 11 | /** 12 | * Jwt 工具类,生成 JWT 和认证 13 | * 14 | * @author hsuyeung 15 | * @date 2020/10/16 16 | */ 17 | public final class JwtUtil { 18 | 19 | /** 20 | * 获取 token 中的所有字段值 21 | * 22 | * @param token token 23 | * @param secret 加密密钥 24 | * @return 所有字段的值 25 | */ 26 | public static Map getClaims(String token, String secret) { 27 | JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret)).build(); 28 | return verifier.verify(token).getClaims(); 29 | } 30 | 31 | /** 32 | * 从 token 中获取指定字段的值 33 | * 34 | * @param token token 35 | * @param key 字段的名字 36 | * @param clazz 字段值的类型 37 | * @param secret 加密密钥 38 | * @param 字段值的类型 39 | * @return 指定字段的值 40 | */ 41 | public static T getClaim(String token, String key, Class clazz, String secret) { 42 | Map claimMap = getClaims(token, secret); 43 | return claimMap.get(key).as(clazz); 44 | } 45 | 46 | private JwtUtil() { 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/user/CreateUserRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.user; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import java.io.Serializable; 10 | 11 | /** 12 | * 创建用户请求参数 13 | * 14 | * @author hsuyeung 15 | * @date 2022/06/29 16 | */ 17 | @ApiModel(description = "创建用户请求参数") 18 | @Data 19 | public class CreateUserRequestDTO implements Serializable { 20 | private static final long serialVersionUID = 7842116972819958320L; 21 | 22 | @ApiModelProperty(value = "用户名", required = true) 23 | @NotBlank(message = "用户名不能为空") 24 | private String username; 25 | 26 | @ApiModelProperty(value = "昵称", required = true) 27 | @NotBlank(message = "昵称不能为空") 28 | private String nickname; 29 | 30 | @ApiModelProperty(value = "密码", required = true) 31 | @NotBlank(message = "密码不能为空") 32 | private String password; 33 | 34 | @ApiModelProperty(value = "确认密码", required = true) 35 | @NotBlank(message = "确认密码不能为空") 36 | private String reconfirmPassword; 37 | 38 | @ApiModelProperty(value = "用户是否可用", required = true) 39 | @NotNull(message = "用户是否可用不能为空") 40 | private Boolean enabled; 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/entity/UserEntity.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.TableField; 4 | import com.baomidou.mybatisplus.annotation.TableName; 5 | import com.hsuyeung.blog.constant.enums.LogicSwitchEnum; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.NoArgsConstructor; 10 | import lombok.experimental.Accessors; 11 | import lombok.experimental.SuperBuilder; 12 | 13 | /** 14 | * 后台用户 15 | * 16 | * @author hsuyeung 17 | * @date 2022/06/28 18 | */ 19 | @Data 20 | @EqualsAndHashCode(callSuper = true) 21 | @SuperBuilder 22 | @NoArgsConstructor 23 | @AllArgsConstructor 24 | @Accessors(chain = true) 25 | @TableName("t_user") 26 | public class UserEntity extends BaseEntity { 27 | private static final long serialVersionUID = 948629388175233044L; 28 | 29 | /** 30 | * 用户名 31 | */ 32 | @TableField("username") 33 | private String username; 34 | 35 | /** 36 | * 登录密码 37 | */ 38 | @TableField("password") 39 | private String password; 40 | 41 | /** 42 | * 昵称 43 | */ 44 | @TableField("nickname") 45 | private String nickname; 46 | 47 | /** 48 | * 是否可用 49 | */ 50 | @TableField("is_enabled") 51 | private LogicSwitchEnum enabled; 52 | } 53 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/article/article_module.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

文章管理

4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/service/IUserRoleService.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.hsuyeung.blog.model.entity.UserRoleEntity; 5 | 6 | import javax.validation.constraints.NotNull; 7 | import java.util.Map; 8 | import java.util.Set; 9 | 10 | /** 11 | * 用户-角色服务接口 12 | * 13 | * @author hsuyeung 14 | * @date 2022/06/28 15 | */ 16 | public interface IUserRoleService extends IService { 17 | /** 18 | * 查询指定用户 id 的角色 id 集合 19 | * 20 | * @param uid 用户 id 21 | * @param onlyQueryEnabled 是否只查询有效的用户 id 关联的角色 id 集合 22 | * @return 用户所拥有的角色 id 集合 23 | */ 24 | Set getRoleIds(@NotNull(message = "uid 不能为 null") Long uid, boolean onlyQueryEnabled); 25 | 26 | /** 27 | * 根据用户 id 删除角色 28 | * 29 | * @param uid 用户 id 30 | * @return 删除成功返回 true,否则返回 false 31 | */ 32 | boolean deleteByUid(Long uid); 33 | 34 | /** 35 | * 根据角色 id 删除用户 36 | * 37 | * @param rid 角色 id 38 | * @return 删除成功返回 true,否则返回 false 39 | */ 40 | boolean deleteByRid(Long rid); 41 | 42 | /** 43 | * 获取指定用户拥有的角色集合 44 | * 45 | * @param uids 用户 id 集合 46 | * @return key-用户 id,value-角色 id 集合 47 | */ 48 | Map> getUidRidsMap(Set uids); 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/service/IRolePermissionService.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.hsuyeung.blog.model.entity.RolePermissionEntity; 5 | 6 | import javax.validation.constraints.NotEmpty; 7 | import java.util.Collection; 8 | import java.util.Set; 9 | 10 | /** 11 | * 角色-权限服务接口 12 | * 13 | * @author hsuyeung 14 | * @date 2022/06/28 15 | */ 16 | public interface IRolePermissionService extends IService { 17 | /** 18 | * 获取指定角色 id 的权限 id 集合 19 | * 20 | * @param rids 角色 id 集合 21 | * @param onlyQueryEnabled 是否只查询可用的角色 id 关联的权限 id 集合 22 | * @return 权限 id 集合 23 | */ 24 | Set getPermissionIds(@NotEmpty(message = "rids 不能为空") Collection rids, boolean onlyQueryEnabled); 25 | 26 | /** 27 | * 根据角色 id 删除权限 28 | * 29 | * @param rid 角色 id 30 | * @return 删除成功返回 true,否则返回 false 31 | */ 32 | boolean deleteByRid(Long rid); 33 | 34 | /** 35 | * 根据权限 id 删除 36 | * 37 | * @param pid 权限 id 38 | * @return 删除成功返回 true,否则返回 false1 39 | */ 40 | boolean deleteByPid(Long pid); 41 | 42 | /** 43 | * 获取拥有指定权限的所有角色 id 44 | * 45 | * @param pid 权限 id 46 | * @return 角色 id 集合 47 | */ 48 | Set getRoleIds(Long pid); 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/permission/UpdatePermissionRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.permission; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import java.io.Serializable; 10 | 11 | /** 12 | * 更新权限请求参数 13 | * 14 | * @author hsuyeung 15 | * @date 2022/06/30 16 | */ 17 | @ApiModel(description = "更新权限请求参数") 18 | @Data 19 | public class UpdatePermissionRequestDTO implements Serializable { 20 | private static final long serialVersionUID = -3304773670047986834L; 21 | 22 | @ApiModelProperty(value = "权限 id", required = true) 23 | @NotNull(message = "id 不能为空") 24 | private Long id; 25 | 26 | @ApiModelProperty(value = "HTTP 方法类型", required = true) 27 | @NotBlank(message = "HTTP 方法类型不能为空") 28 | private String method; 29 | 30 | @ApiModelProperty(value = "接口路径", required = true) 31 | @NotBlank(message = "接口不能为空") 32 | private String path; 33 | 34 | @ApiModelProperty(value = "接口描述", required = true) 35 | @NotBlank(message = "接口描述不能为空") 36 | private String permissionDesc; 37 | 38 | @ApiModelProperty(value = "权限是否可用", required = true) 39 | @NotNull(message = "权限是否可用不能为空") 40 | private Boolean enabled; 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/entity/PermissionEntity.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.TableField; 4 | import com.baomidou.mybatisplus.annotation.TableName; 5 | import com.hsuyeung.blog.constant.enums.LogicSwitchEnum; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.NoArgsConstructor; 10 | import lombok.experimental.Accessors; 11 | import lombok.experimental.SuperBuilder; 12 | 13 | /** 14 | * 接口权限表 15 | * 16 | * @author hsuyeung 17 | * @date 2022/06/28 18 | */ 19 | @Data 20 | @EqualsAndHashCode(callSuper = true) 21 | @SuperBuilder 22 | @NoArgsConstructor 23 | @AllArgsConstructor 24 | @Accessors(chain = true) 25 | @TableName("t_permission") 26 | public class PermissionEntity extends BaseEntity { 27 | private static final long serialVersionUID = 43097764803444971L; 28 | 29 | /** 30 | * 接口路径 31 | */ 32 | @TableField("path") 33 | private String path; 34 | 35 | /** 36 | * HTTP 方法类型 37 | */ 38 | @TableField("method") 39 | private String method; 40 | 41 | /** 42 | * 接口描述 43 | */ 44 | @TableField("permission_desc") 45 | private String permissionDesc; 46 | 47 | /** 48 | * 是否可用 49 | */ 50 | @TableField("is_enabled") 51 | private LogicSwitchEnum enabled; 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/mail/MailInfoVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.mail; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * 分页列表邮件信息 11 | * 12 | * @author hsuyeung 13 | * @date 2022/07/06 14 | */ 15 | @ApiModel(description = "分页列表邮件信息") 16 | @Data 17 | public class MailInfoVO implements Serializable { 18 | private static final long serialVersionUID = -5548040036708028953L; 19 | 20 | @ApiModelProperty("id") 21 | private Long id; 22 | 23 | @ApiModelProperty("发件人") 24 | private String mFrom; 25 | 26 | @ApiModelProperty("收件人") 27 | private String mTo; 28 | 29 | @ApiModelProperty("主题") 30 | private String mSubject; 31 | 32 | @ApiModelProperty("内容预览地址") 33 | private String textPreviewUrl; 34 | 35 | @ApiModelProperty("抄送") 36 | private String cc; 37 | 38 | @ApiModelProperty("密送") 39 | private String bcc; 40 | 41 | @ApiModelProperty("状态") 42 | private String mStatus; 43 | 44 | @ApiModelProperty("错误信息") 45 | private String errorMsg; 46 | 47 | @ApiModelProperty("邮件类型") 48 | private String type; 49 | 50 | @ApiModelProperty("发送时间") 51 | private String sendTime; 52 | 53 | @ApiModelProperty("重试次数") 54 | private Integer retryNum; 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/config/RequestConfigConfig.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.config; 2 | 3 | import com.hsuyeung.blog.config.properties.RequestConfigProperties; 4 | import com.hsuyeung.blog.util.HttpClientUtil; 5 | import lombok.RequiredArgsConstructor; 6 | import org.apache.http.client.config.RequestConfig; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.Primary; 10 | 11 | /** 12 | * 根据 {@link RequestConfigProperties} 自定义的配置定义 RequestConfig Bean,用于注入到 {@link HttpClientUtil} 中使用 13 | * 14 | * @author hsuyeung 15 | * @date 2022/04/13 16 | */ 17 | @Configuration 18 | @RequiredArgsConstructor 19 | public class RequestConfigConfig { 20 | private final RequestConfigProperties requestConfigProperties; 21 | 22 | /** 23 | * 自定义的 RequestConfig Bean 24 | * 25 | * @return {@link RequestConfig} 26 | */ 27 | @Bean 28 | @Primary 29 | public RequestConfig customRequestConfig() { 30 | return RequestConfig.custom() 31 | .setConnectionRequestTimeout(requestConfigProperties.getConnectionRequestTimeout()) 32 | .setConnectTimeout(requestConfigProperties.getConnectTimeout()) 33 | .setSocketTimeout(requestConfigProperties.getSocketTimeout()) 34 | .build(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/systemconfig/SystemConfigInfoVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.systemconfig; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * 分页列表系统配置信息 11 | * 12 | * @author hsuyeung 13 | * @date 2022/07/06 14 | */ 15 | @ApiModel(description = "分页列表系统配置信息") 16 | @Data 17 | public class SystemConfigInfoVO implements Serializable { 18 | private static final long serialVersionUID = 5297155034521019447L; 19 | 20 | @ApiModelProperty("系统配置 id") 21 | private Long id; 22 | 23 | @ApiModelProperty("配置 key") 24 | private String confKey; 25 | 26 | @ApiModelProperty("配置 value") 27 | private String confValue; 28 | 29 | @ApiModelProperty("配置默认 value") 30 | private String confDefaultValue; 31 | 32 | @ApiModelProperty("配置分组") 33 | private String confGroup; 34 | 35 | @ApiModelProperty("配置描述") 36 | private String description; 37 | 38 | @ApiModelProperty("是否可用") 39 | private Boolean enabled; 40 | 41 | @ApiModelProperty("创建时间") 42 | private String createTime; 43 | 44 | @ApiModelProperty("创建人") 45 | private String createBy; 46 | 47 | @ApiModelProperty("更新时间") 48 | private String updateTime; 49 | 50 | @ApiModelProperty("更新人") 51 | private String updateBy; 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/mail/MailSearchDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.mail; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiParam; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.Size; 8 | import java.io.Serializable; 9 | 10 | /** 11 | * 邮件分页搜索条件 12 | * 13 | * @author hsuyeung 14 | * @since 2023/8/26 15 | */ 16 | @Data 17 | @ApiModel(description = "邮件分页搜索条件") 18 | public class MailSearchDTO implements Serializable { 19 | private static final long serialVersionUID = 5751405303584710345L; 20 | 21 | @ApiParam("发件人") 22 | @Size(max = 255, message = "发件人不能超过 255 个字符") 23 | private String from; 24 | 25 | @ApiParam("收件人") 26 | @Size(max = 2550, message = "收件人不能超过 2550 个字符") 27 | private String to; 28 | 29 | @ApiParam("主题") 30 | @Size(max = 255, message = "主题不能超过 255 个字符") 31 | private String subject; 32 | 33 | @ApiParam("抄送") 34 | @Size(max = 2550, message = "抄送不能超过 2550 个字符") 35 | private String cc; 36 | 37 | @ApiParam("密送") 38 | @Size(max = 2550, message = "密送不能超过 2550 个字符") 39 | private String bcc; 40 | 41 | @ApiParam("状态") 42 | private Integer status; 43 | 44 | @ApiParam("类型") 45 | private Integer type; 46 | 47 | @ApiParam("开始时间戳") 48 | private Long startTimestamp; 49 | 50 | @ApiParam("结束时间戳") 51 | private Long endTimestamp; 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/entity/FriendLinkEntity.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.TableField; 4 | import com.baomidou.mybatisplus.annotation.TableName; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.NoArgsConstructor; 9 | import lombok.experimental.Accessors; 10 | import lombok.experimental.SuperBuilder; 11 | 12 | /** 13 | * 友链表 14 | * 15 | * @author hsuyeung 16 | * @date 2022/06/22 17 | */ 18 | @Data 19 | @EqualsAndHashCode(callSuper = true) 20 | @SuperBuilder 21 | @NoArgsConstructor 22 | @AllArgsConstructor 23 | @Accessors(chain = true) 24 | @TableName("t_friend_link") 25 | public class FriendLinkEntity extends BaseEntity { 26 | private static final long serialVersionUID = 2956858940933275569L; 27 | 28 | /** 29 | * 友链名称 30 | */ 31 | @TableField("link_name") 32 | private String linkName; 33 | 34 | /** 35 | * 友链链接 36 | */ 37 | @TableField("link_url") 38 | private String linkUrl; 39 | 40 | /** 41 | * 友链头像 42 | */ 43 | @TableField("link_avatar") 44 | private String linkAvatar; 45 | 46 | /** 47 | * 一句话描述 48 | */ 49 | @TableField("link_desc") 50 | private String linkDesc; 51 | 52 | /** 53 | * 友链分组 54 | */ 55 | @TableField("link_group") 56 | private String linkGroup; 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/service/IUserTokenService.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.service; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import javax.validation.constraints.NotNull; 5 | import java.time.LocalDateTime; 6 | 7 | /** 8 | * 用户 token 相关服务 9 | * 10 | * @author hsuyeung 11 | * @date 2022/05/19 12 | */ 13 | public interface IUserTokenService { 14 | 15 | /** 16 | * 生成用户相关 token,并将其存入 redis 的 hash 结构中 17 | *

目前的登录策略:每访问一次登录接口就将之前的 token 删除然后存入新的 token

18 | * 19 | * @param userId 用户 id 20 | * @param expiresAt 该 token 在什么时候过期 21 | * @return 生成的 token 22 | */ 23 | String generateUserToken(Long userId, LocalDateTime expiresAt); 24 | 25 | /** 26 | * 判断用户 token 是否过期 27 | * 28 | * @param token 用户 token 29 | * @return 如果 token 解析失败、已经到达过期时间、redis 中不存在该用户的 token 或是 redis 中的 token 与传入的 token 不相等则返回 true,否则返回 false 30 | */ 31 | boolean isExpired(String token); 32 | 33 | /** 34 | * 删除指定用户 token 35 | * 36 | * @param uid 用户 id 37 | * @return 删除成功返回 true,否则返回 false 38 | */ 39 | boolean deleteUserToken(Long uid); 40 | 41 | /** 42 | * 从 request 的 header 中获取 token 中的用户 id 43 | * 44 | * @param request {@link HttpServletRequest} 45 | * @return 用户 id 46 | */ 47 | Long getUserIdFromRequestHeader(@NotNull(message = "request 不能为 null") HttpServletRequest request); 48 | } 49 | -------------------------------------------------------------------------------- /src/main/resources/static/fonts/iconmoon/icomoon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/user/UpdateUserRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.user; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import java.io.Serializable; 10 | 11 | /** 12 | * 更新用户信息请求参数 13 | * 14 | * @author hsuyeung 15 | * @date 2022/06/29 16 | */ 17 | @ApiModel(description = "更新用户信息请求参数") 18 | @Data 19 | public class UpdateUserRequestDTO implements Serializable { 20 | private static final long serialVersionUID = -6710749659912700292L; 21 | 22 | @ApiModelProperty(value = "用户 id", required = true) 23 | @NotNull(message = "用户 id 不能为空") 24 | private Long id; 25 | 26 | @ApiModelProperty(value = "用户名", required = true) 27 | @NotBlank(message = "用户名不能为空") 28 | private String username; 29 | 30 | @ApiModelProperty("旧密码") 31 | private String oldPassword; 32 | 33 | @ApiModelProperty("新密码") 34 | private String newPassword; 35 | 36 | @ApiModelProperty("再次确认新密码") 37 | private String reconfirmNewPassword; 38 | 39 | @ApiModelProperty(value = "昵称", required = true) 40 | @NotBlank(message = "用户昵称不能为空") 41 | private String nickname; 42 | 43 | @ApiModelProperty(value = "是否可用", required = true) 44 | @NotNull(message = "是否可用不能为空") 45 | private Boolean enabled; 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/permission/CreatePermissionDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.permission; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import javax.validation.constraints.Size; 10 | import java.io.Serializable; 11 | 12 | /** 13 | * 创建权限请求参数 14 | * 15 | * @author hsuyeung 16 | * @date 2022/06/30 17 | */ 18 | @ApiModel(description = "创建权限请求参数") 19 | @Data 20 | public class CreatePermissionDTO implements Serializable { 21 | private static final long serialVersionUID = 3971000705974467539L; 22 | 23 | @ApiModelProperty(value = "接口路径", required = true) 24 | @NotBlank(message = "接口路径不能为空") 25 | @Size(max = 255, message = "接口路径不能超过 255 个字符") 26 | private String path; 27 | 28 | @ApiModelProperty(value = "HTTP 方法类型", required = true) 29 | @NotBlank(message = "HTTP 方法类型不能为空") 30 | @Size(max = 32, message = "HTTP 方法类型不能超过 32 个字符") 31 | private String method; 32 | 33 | @ApiModelProperty(value = "接口描述", required = true) 34 | @NotBlank(message = "接口描述不能为空") 35 | @Size(max = 255, message = "接口描述不能超过 255 个字符") 36 | private String permissionDesc; 37 | 38 | @ApiModelProperty(value = "是否可用", required = true) 39 | @NotNull(message = "是否可用不能为 null") 40 | private Boolean enabled; 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/cache/FriendLinkCache.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.cache; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.hsuyeung.blog.exception.SystemInternalException; 6 | import com.hsuyeung.blog.model.vo.friendlink.FriendLinkVO; 7 | import com.hsuyeung.blog.util.RedisUtil; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.stereotype.Component; 10 | 11 | /** 12 | * 友链缓存 13 | * 14 | * @author hsuyeung 15 | * @date 2022/06/23 16 | */ 17 | @Component 18 | @RequiredArgsConstructor 19 | public class FriendLinkCache { 20 | private final RedisUtil redisUtil; 21 | private final ObjectMapper objectMapper; 22 | 23 | 24 | public FriendLinkVO getFriendLinkVO(String key) { 25 | String value = (String) redisUtil.get(key); 26 | try { 27 | return value == null ? null : objectMapper.readValue(value, FriendLinkVO.class); 28 | } catch (JsonProcessingException e) { 29 | throw new SystemInternalException("反序列化 FriendLinkVO 对象失败", e); 30 | } 31 | } 32 | 33 | public void cacheFriendLinkVO(String key, FriendLinkVO friendLinkVO) { 34 | try { 35 | redisUtil.set(key, objectMapper.writeValueAsString(friendLinkVO)); 36 | } catch (JsonProcessingException e) { 37 | throw new SystemInternalException("序列化 FriendLinkVO 对象失败", e); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/config/ThreadPoolConfig.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.config; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.scheduling.annotation.EnableAsync; 7 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 8 | 9 | import java.util.concurrent.Executor; 10 | import java.util.concurrent.ThreadPoolExecutor; 11 | 12 | /** 13 | * 线程池配置 14 | * 15 | * @author hsuyeung 16 | * @date 2020/11/12 15:36 17 | */ 18 | @Configuration 19 | @EnableAsync 20 | @Slf4j 21 | public class ThreadPoolConfig { 22 | 23 | @Bean 24 | public Executor asyncServiceExecutor() { 25 | log.info("start asyncServiceExecutor..."); 26 | ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 27 | // 配置核心线程数 28 | executor.setCorePoolSize(4); 29 | // 配置最大线程数 30 | executor.setMaxPoolSize(8); 31 | // 配置队列大小 32 | executor.setQueueCapacity(10000); 33 | // 配置线程池中的线程的名称前缀 34 | executor.setThreadNamePrefix("async-thread-"); 35 | 36 | // rejection-policy:当 pool 已经达到 max size 的时候,如何处理新任务 37 | // 抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务 38 | executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy()); 39 | // 执行初始化 40 | executor.initialize(); 41 | return executor; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/resources/static/css/toc.min.css: -------------------------------------------------------------------------------- 1 | .toc-wrapper{position:absolute;width:28rem;right:-28.8rem;top:0}.toc{position:fixed;font-size:1.6rem;max-width:28rem;min-width:20rem;overflow:auto;background-color:#faf5e3;max-height:90%}.toc-inner ul{overflow:auto;list-style:none;padding-left:1.2rem}.toc-inner{overflow:auto;padding:.6rem .4rem .6rem 0}.toc .box{margin-right:.4rem}.toc .active{background:#e7e1c8}.toc-widget ol,.toc-widget ul{padding-left:2rem;margin-top:.8rem}.toc ol ul,.toc ul ul{margin-top:.5rem}.toc ul li ul{margin:.5rem 0}.toc-h1{margin-left:0}.toc-h2{margin-left:1rem}.toc-h3{margin-left:2rem}.toc-h4{margin-left:3rem}.toc-h5{margin-left:4rem}.toc-h6{margin-left:5rem}.toc .toc-head{font-size:1.5rem;border-bottom:.1rem solid rgba(154,128,92,.7);background-color:#faf5e3;top:0;position:sticky;padding:.3rem 1rem}.toc .toc-head-top{float:right}.toc .toc-tail{font-size:1.5rem;border-top:.1rem solid rgba(154,128,92,.7);background-color:#faf5e3;bottom:0;padding:.3rem 1rem;position:sticky}.toc .toc-tail-contact{float:right}.toc .toc-widget{font-size:1.5rem;border-top:.1rem solid rgba(154,128,92,.7);padding:.8rem 1rem}.toc .toc-widget li{margin-top:.4rem}@media (max-width:1450px){.toc-inner ul{padding-left:0}}@media (max-width:1100px){.toc-wrapper{display:none}}@media print{.toc-wrapper{display:none}}.toc li.active .box{background-color:#2ecc71}.toc li.active .box,.toc li .box{display:inline-block;width:1rem;height:1rem;border-radius:4rem;vertical-align:middle}.toc li .box{background-color:#f39c12} 2 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/article/ArticleInfoVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.article; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * 分页列表文章数据 11 | * 12 | * @author hsuyeung 13 | * @date 2022/07/08 14 | */ 15 | @ApiModel(description = "分页列表文章数据") 16 | @Data 17 | public class ArticleInfoVO implements Serializable { 18 | private static final long serialVersionUID = -5021157866977931188L; 19 | 20 | @ApiModelProperty("文章 id") 21 | private Long id; 22 | 23 | @ApiModelProperty("文章标题") 24 | private String title; 25 | 26 | @ApiModelProperty("文章路由") 27 | private String route; 28 | 29 | @ApiModelProperty("文章作者") 30 | private String author; 31 | 32 | @ApiModelProperty("文章关键词") 33 | private String keywords; 34 | 35 | @ApiModelProperty("文章描述") 36 | private String description; 37 | 38 | @ApiModelProperty("文章 url") 39 | private String url; 40 | 41 | @ApiModelProperty("是否置顶") 42 | private Boolean pin; 43 | 44 | @ApiModelProperty("评论数") 45 | private Long commentNum; 46 | 47 | @ApiModelProperty("创建时间") 48 | private String createTime; 49 | 50 | @ApiModelProperty("创建人") 51 | private String createBy; 52 | 53 | @ApiModelProperty("更新时间") 54 | private String updateTime; 55 | 56 | @ApiModelProperty("更新人") 57 | private String updateBy; 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/vo/comment/CommentInfoVO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.vo.comment; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * 分页列表评论信息 11 | * 12 | * @author hsuyeung 13 | * @date 2022/07/07 14 | */ 15 | @ApiModel(description = "分页列表评论信息") 16 | @Data 17 | public class CommentInfoVO implements Serializable { 18 | private static final long serialVersionUID = 2818155637960948938L; 19 | 20 | @ApiModelProperty("评论 id") 21 | private Long id; 22 | 23 | @ApiModelProperty("昵称") 24 | private String nickname; 25 | 26 | @ApiModelProperty("评论者头像") 27 | private String avatar; 28 | 29 | @ApiModelProperty("评论者邮箱") 30 | private String email; 31 | 32 | @ApiModelProperty("评论者网站地址") 33 | private String website; 34 | 35 | @ApiModelProperty("评论内容预览地址") 36 | private String contentPreviewUrl; 37 | 38 | @ApiModelProperty("父级评论人的昵称") 39 | private String parentNickname; 40 | 41 | @ApiModelProperty("回复的评论人的昵称") 42 | private String replyNickname; 43 | 44 | @ApiModelProperty("文章标题") 45 | private String articleTitle; 46 | 47 | @ApiModelProperty("文章地址") 48 | private String articleUrl; 49 | 50 | @ApiModelProperty("评论者的 ip 地址") 51 | private String ip; 52 | 53 | @ApiModelProperty("是否通过邮件接收回复提醒") 54 | private Boolean notification; 55 | 56 | @ApiModelProperty("评论时间") 57 | private String createTime; 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/entity/SystemConfigEntity.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.TableField; 4 | import com.baomidou.mybatisplus.annotation.TableName; 5 | import com.hsuyeung.blog.constant.enums.LogicSwitchEnum; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.NoArgsConstructor; 10 | import lombok.experimental.Accessors; 11 | import lombok.experimental.SuperBuilder; 12 | 13 | /** 14 | *

15 | * 系统配置表 16 | *

17 | * 18 | * @author hsuyeung 19 | * @since 2022/06/05 20 | */ 21 | @Data 22 | @EqualsAndHashCode(callSuper = true) 23 | @SuperBuilder 24 | @NoArgsConstructor 25 | @AllArgsConstructor 26 | @Accessors(chain = true) 27 | @TableName("t_system_config") 28 | public class SystemConfigEntity extends BaseEntity { 29 | private static final long serialVersionUID = -3367835041861760759L; 30 | 31 | /** 32 | * 配置 key 33 | */ 34 | @TableField("conf_key") 35 | private String confKey; 36 | 37 | /** 38 | * 配置 value 39 | */ 40 | @TableField("conf_value") 41 | private String confValue; 42 | 43 | /** 44 | * 配置分组 45 | */ 46 | @TableField("conf_group") 47 | private String confGroup; 48 | 49 | /** 50 | * 配置描述 51 | */ 52 | @TableField("`description`") 53 | private String description; 54 | 55 | /** 56 | * 是否有效(0:无效;1:有效) 57 | */ 58 | @TableField("is_enabled") 59 | private LogicSwitchEnum enabled; 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/friendlink/AddFriendLinkDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.friendlink; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.Size; 9 | import java.io.Serializable; 10 | 11 | /** 12 | * 添加友链请求参数实体类 13 | * 14 | * @author hsuyeung 15 | * @date 2022/06/25 16 | */ 17 | @ApiModel(description = "添加友链请求参数") 18 | @Data 19 | public class AddFriendLinkDTO implements Serializable { 20 | private static final long serialVersionUID = -1770340147253174130L; 21 | 22 | @ApiModelProperty(value = "友链名称", required = true) 23 | @NotBlank(message = "友链名称不能为空") 24 | @Size(max = 255, message = "友链名称不能超过 255 个字符") 25 | private String linkName; 26 | 27 | @ApiModelProperty(value = "友链链接", required = true) 28 | @NotBlank(message = "友链链接不能为空") 29 | @Size(max = 255, message = "友链链接不能超过 255 个字符") 30 | private String linkUrl; 31 | 32 | @ApiModelProperty(value = "友链头像", required = true) 33 | @NotBlank(message = "友链头像不能为空") 34 | @Size(max = 255, message = "友链头像不能超过 255 个字符") 35 | private String linkAvatar; 36 | 37 | @ApiModelProperty("一句话描述") 38 | @Size(max = 255, message = "一句话描述不能超过 255 个字符") 39 | private String linkDesc; 40 | 41 | @ApiModelProperty(value = "友链分组", required = true) 42 | @NotBlank(message = "友链分组不能为空") 43 | @Size(max = 255, message = "友链分组不能超过 255 个字符") 44 | private String linkGroup; 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/web/controller/AdminPageController.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.web.controller; 2 | 3 | import com.hsuyeung.blog.service.IMailService; 4 | import com.hsuyeung.blog.service.ISystemConfigService; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.servlet.ModelAndView; 10 | 11 | import static com.hsuyeung.blog.constant.SystemConfigConstants.SystemConfigEnum.SYSTEM_BROWSER_STATIC_RESOURCE_VERSION; 12 | 13 | /** 14 | * 管理后台页面控制器 15 | * 16 | * @author hsuyeung 17 | * @date 2022/06/29 18 | */ 19 | @Controller 20 | @RequestMapping("/admin") 21 | @RequiredArgsConstructor 22 | public class AdminPageController { 23 | private final ISystemConfigService systemConfigService; 24 | private final IMailService mailService; 25 | 26 | @RequestMapping("/home") 27 | public ModelAndView index() { 28 | ModelAndView mv = new ModelAndView("admin/home"); 29 | mv.addObject("v", systemConfigService.getConfigValue(SYSTEM_BROWSER_STATIC_RESOURCE_VERSION, String.class)); 30 | return mv; 31 | } 32 | 33 | @RequestMapping("/preview/mail/text/{mailId}") 34 | public ModelAndView previewMailText(@PathVariable("mailId") Long mailId) { 35 | ModelAndView mv = new ModelAndView("admin/mail_preview"); 36 | mv.addObject("text", mailService.getMailText(mailId)); 37 | return mv; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/config/properties/RequestConfigProperties.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.config.properties; 2 | 3 | import lombok.Data; 4 | import org.apache.http.client.config.RequestConfig; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | /** 9 | * 自定义 httpclient 的 RequestConfig 配置,只配置了常用的几个参数。 10 | *

需要自定义配置其他参数在这里加上之后,再去 RequestConfigConfig 中配置即可

11 | * 12 | * @author hsuyeung 13 | * @date 2022/04/13 14 | */ 15 | @Data 16 | @Configuration 17 | @ConfigurationProperties(prefix = "http.client.request.config") 18 | public class RequestConfigProperties { 19 | /** 20 | * Timeout to get a connection from the connection manager for the Http Client 21 | * The connection manager could be a pool like PoolingHttpClientConnectionManager. 22 | * When all connections from the pool are used, then the ConnectionRequestTimeout indicates how long your code should 23 | * wait for a connection to be freed up. 24 | * 25 | * @see RequestConfig#getConnectionRequestTimeout() 26 | */ 27 | private int connectionRequestTimeout = -1; 28 | 29 | /** 30 | * Connection Timeout to establish a connection with the server. 31 | * 32 | * @see RequestConfig#getConnectTimeout() 33 | */ 34 | private int connectTimeout = -1; 35 | 36 | /** 37 | * Timeout between two data packets from the server 38 | * 39 | * @see RequestConfig#getSocketTimeout() 40 | */ 41 | private int socketTimeout = -1; 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/permission/UpdatePermissionDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.permission; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import javax.validation.constraints.Size; 10 | import java.io.Serializable; 11 | 12 | /** 13 | * 更新权限请求参数 14 | * 15 | * @author hsuyeung 16 | * @date 2022/06/30 17 | */ 18 | @ApiModel(description = "更新权限请求参数") 19 | @Data 20 | public class UpdatePermissionDTO implements Serializable { 21 | private static final long serialVersionUID = -3304773670047986834L; 22 | 23 | @ApiModelProperty(value = "权限 id", required = true) 24 | @NotNull(message = "id 不能为 null") 25 | private Long id; 26 | 27 | @ApiModelProperty(value = "HTTP 方法类型", required = true) 28 | @NotBlank(message = "HTTP 方法类型不能为空") 29 | @Size(max = 32, message = "HTTP 方法类型不能超过 32 个字符") 30 | private String method; 31 | 32 | @ApiModelProperty(value = "接口路径", required = true) 33 | @NotBlank(message = "接口路径不能为空") 34 | @Size(max = 255, message = "接口路径不能超过 255 个字符") 35 | private String path; 36 | 37 | @ApiModelProperty(value = "接口描述", required = true) 38 | @NotBlank(message = "接口描述不能为空") 39 | @Size(max = 255, message = "接口描述不能超过 255 个字符") 40 | private String permissionDesc; 41 | 42 | @ApiModelProperty(value = "权限是否可用", required = true) 43 | @NotNull(message = "权限是否可用不能为 null") 44 | private Boolean enabled; 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/user/CreateUserDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.user; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import javax.validation.constraints.Size; 10 | import java.io.Serializable; 11 | 12 | /** 13 | * 创建用户请求参数 14 | * 15 | * @author hsuyeung 16 | * @date 2022/06/29 17 | */ 18 | @ApiModel(description = "创建用户请求参数") 19 | @Data 20 | public class CreateUserDTO implements Serializable { 21 | private static final long serialVersionUID = 7842116972819958320L; 22 | 23 | @ApiModelProperty(value = "用户名", required = true) 24 | @NotBlank(message = "用户名不能为空") 25 | @Size(max = 32, message = "用户名不能超过 32 个字符") 26 | private String username; 27 | 28 | @ApiModelProperty(value = "昵称", required = true) 29 | @NotBlank(message = "昵称不能为空") 30 | @Size(max = 64, message = "昵称不能超过 64 个字符") 31 | private String nickname; 32 | 33 | @ApiModelProperty(value = "密码", required = true) 34 | @NotBlank(message = "密码不能为空") 35 | @Size(max = 16, message = "密码不能超过 16 个字符") 36 | private String password; 37 | 38 | @ApiModelProperty(value = "确认密码", required = true) 39 | @NotBlank(message = "确认密码不能为空") 40 | @Size(max = 16, message = "密码不能超过 16 个字符") 41 | private String reconfirmPassword; 42 | 43 | @ApiModelProperty(value = "用户是否可用", required = true) 44 | @NotNull(message = "用户是否可用不能为 null") 45 | private Boolean enabled; 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/schedule/SendFailedEmailRetrySchedule.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.schedule; 2 | 3 | import com.hsuyeung.blog.model.entity.MailEntity; 4 | import com.hsuyeung.blog.service.IMailService; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.scheduling.annotation.Scheduled; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.List; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | /** 14 | * 发送失败的邮件重试定时任务 15 | * 16 | * @author hsuyeung 17 | * @date 2022/10/18 18 | */ 19 | @Component 20 | @Slf4j 21 | @RequiredArgsConstructor 22 | public class SendFailedEmailRetrySchedule { 23 | private final IMailService mailService; 24 | 25 | /** 26 | * 十分钟检查一次是否有发送失败的邮件 27 | */ 28 | @Scheduled(fixedRate = 10, timeUnit = TimeUnit.MINUTES) 29 | public void task() { 30 | log.info("失败邮件重发定时任务开始"); 31 | List sendMailList = mailService.listAllSendFailedAndLtMaxRetryNum(); 32 | if (sendMailList.isEmpty()) { 33 | log.info("失败邮件重发定时任务结束:没有发送失败或者重试次数小于最大重试次数的邮件"); 34 | return; 35 | } 36 | sendMailList.forEach(mailEntity -> { 37 | mailService.retrySendFailedEmail(mailEntity, true); 38 | try { 39 | Thread.sleep(500); 40 | } catch (InterruptedException e) { 41 | log.error("线程休眠失败", e); 42 | Thread.currentThread().interrupt(); 43 | } 44 | }); 45 | log.info("失败邮件重发定时任务结束:执行完成"); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/util/ApplicationContextProvider.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.util; 2 | 3 | import org.springframework.beans.BeansException; 4 | import org.springframework.context.ApplicationContext; 5 | import org.springframework.context.ApplicationContextAware; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * @author hsuyeung 10 | * @date 2021/1/31 12:46 11 | */ 12 | @Component 13 | public class ApplicationContextProvider implements ApplicationContextAware { 14 | /** 15 | * 上下文对象实例 16 | */ 17 | private static ApplicationContext applicationContext; 18 | 19 | @Override 20 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 21 | ApplicationContextProvider.applicationContext = applicationContext; 22 | } 23 | 24 | /** 25 | * 获取 applicationContext 26 | */ 27 | public static ApplicationContext getApplicationContext() { 28 | return applicationContext; 29 | } 30 | 31 | /** 32 | * 通过 name 获取 Bean. 33 | */ 34 | public static Object getBean(String name) { 35 | return getApplicationContext().getBean(name); 36 | } 37 | 38 | /** 39 | * 通过 class 获取 Bean. 40 | */ 41 | public static T getBean(Class clazz) { 42 | return getApplicationContext().getBean(clazz); 43 | } 44 | 45 | /** 46 | * 通过 name,以及 Clazz 返回指定的 Bean 47 | */ 48 | public static T getBean(String name, Class clazz) { 49 | return getApplicationContext().getBean(name, clazz); 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/web/core/HeaderMapRequestWrapper.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.web.core; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import javax.servlet.http.HttpServletRequestWrapper; 5 | import java.util.Collections; 6 | import java.util.Enumeration; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | 10 | /** 11 | * @author hsuyeung 12 | * @date 2022/12/23 13 | */ 14 | public class HeaderMapRequestWrapper extends HttpServletRequestWrapper { 15 | private final HashMap headerMap = new HashMap<>(); 16 | 17 | public HeaderMapRequestWrapper(HttpServletRequest request) { 18 | super(request); 19 | } 20 | 21 | public void addHeader(String name, String value) { 22 | headerMap.put(name, value); 23 | } 24 | 25 | @Override 26 | public String getHeader(String name) { 27 | return headerMap.containsKey(name) 28 | ? headerMap.get(name) 29 | : super.getHeader(name); 30 | } 31 | 32 | 33 | @Override 34 | public Enumeration getHeaderNames() { 35 | List names = Collections.list(super.getHeaderNames()); 36 | names.addAll(headerMap.keySet()); 37 | return Collections.enumeration(names); 38 | } 39 | 40 | @Override 41 | public Enumeration getHeaders(String name) { 42 | List values = Collections.list(super.getHeaders(name)); 43 | if (headerMap.containsKey(name)) { 44 | values.add(headerMap.get(name)); 45 | } 46 | return Collections.enumeration(values); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/comment/CommentSearchDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.comment; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiParam; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.Size; 8 | import java.io.Serializable; 9 | 10 | /** 11 | * 评论分页搜索条件 12 | * 13 | * @author hsuyeung 14 | * @since 2023/8/26 15 | */ 16 | @Data 17 | @ApiModel(description = "评论分页搜索条件") 18 | public class CommentSearchDTO implements Serializable { 19 | private static final long serialVersionUID = -3396763437530165895L; 20 | 21 | @ApiParam("评论人昵称") 22 | @Size(max = 32, message = "评论者昵称不能超过 32 个字符") 23 | private String nickname; 24 | 25 | @ApiParam("评论人邮箱") 26 | @Size(max = 64, message = "评论人邮箱不能超过 64 个字符") 27 | private String email; 28 | 29 | @ApiParam("评论人网址") 30 | @Size(max = 255, message = "评论人网址不能超过 255 个字符") 31 | private String website; 32 | 33 | @ApiParam("父级评论人昵称") 34 | @Size(max = 32, message = "父级评论人昵称不能超过 32 个字符") 35 | private String parentNickname; 36 | 37 | @ApiParam("回复评论人昵称") 38 | @Size(max = 32, message = "回复评论人昵称不能超过 32 个字符") 39 | private String replyNickname; 40 | 41 | @ApiParam("文章 id") 42 | private Long articleId; 43 | 44 | @ApiParam("ip") 45 | @Size(max = 128, message = "ip 不能超过 128 个字符") 46 | private String ip; 47 | 48 | @ApiParam("是否接收邮件提醒") 49 | private Integer notification; 50 | 51 | @ApiParam("开始时间戳") 52 | private Long startTimestamp; 53 | 54 | @ApiParam("结束时间戳") 55 | private Long endTimestamp; 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/web/api/MailApi.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.web.api; 2 | 3 | import com.hsuyeung.blog.model.dto.PageSearchDTO; 4 | import com.hsuyeung.blog.model.dto.mail.MailSearchDTO; 5 | import com.hsuyeung.blog.model.vo.PageVO; 6 | import com.hsuyeung.blog.model.vo.mail.MailInfoVO; 7 | import com.hsuyeung.blog.service.IMailService; 8 | import com.hsuyeung.blog.web.core.IBaseWebResponse; 9 | import com.hsuyeung.blog.web.core.WebResponse; 10 | import io.swagger.annotations.Api; 11 | import io.swagger.annotations.ApiOperation; 12 | import io.swagger.annotations.ApiParam; 13 | import lombok.RequiredArgsConstructor; 14 | import org.springframework.web.bind.annotation.PostMapping; 15 | import org.springframework.web.bind.annotation.RequestBody; 16 | import org.springframework.web.bind.annotation.RequestMapping; 17 | import org.springframework.web.bind.annotation.RestController; 18 | 19 | /** 20 | * @author hsuyeung 21 | * @date 2022/07/06 22 | */ 23 | @Api(tags = "邮件相关接口") 24 | @RestController 25 | @RequestMapping("/api/mail") 26 | @RequiredArgsConstructor 27 | public class MailApi implements IBaseWebResponse { 28 | private final IMailService mailService; 29 | 30 | /** 31 | * 分页查询邮件列表 32 | * 33 | * @param pageSearchParam 邮件分页搜索条件 34 | * @return 邮件分页列表 35 | */ 36 | @ApiOperation("分页查询邮件列表") 37 | @PostMapping("/actions/page") 38 | public WebResponse> getMailPage( 39 | @ApiParam("发件人") @RequestBody PageSearchDTO pageSearchParam) { 40 | return ok(mailService.getMailPage(pageSearchParam)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/resources/templates/component/admin/user/edit_user_panel.html: -------------------------------------------------------------------------------- 1 | 40 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/config/Swagger2Config.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.config; 2 | 3 | import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import springfox.documentation.builders.ApiInfoBuilder; 7 | import springfox.documentation.builders.PathSelectors; 8 | import springfox.documentation.builders.RequestHandlerSelectors; 9 | import springfox.documentation.service.Contact; 10 | import springfox.documentation.spi.DocumentationType; 11 | import springfox.documentation.spring.web.plugins.Docket; 12 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 13 | 14 | /** 15 | * Swagger 接口文档配置 16 | * 17 | * @author hsuyeung 18 | * @date 2022/05/12 19 | */ 20 | @Configuration 21 | @EnableSwagger2 22 | @EnableKnife4j 23 | public class Swagger2Config { 24 | @Bean(value = "swaggerApi2") 25 | public Docket defaultApi2() { 26 | return new Docket(DocumentationType.SWAGGER_2) 27 | .apiInfo(new ApiInfoBuilder() 28 | .title("博客管理后台接口 api 文档") 29 | .description("博客管理后台接口 api 详细参数说明") 30 | .contact(new Contact("hsuyeung", "hsuyeung.com", "hsuyeung.com@gmail.com")) 31 | .version("1.0") 32 | .build()) 33 | .groupName("博客管理后台接口文档") 34 | .select() 35 | .apis(RequestHandlerSelectors.basePackage("com.hsuyeung.blog.web.api")) 36 | .paths(PathSelectors.any()) 37 | .build(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/interceptor/UserPermissionCheckInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.interceptor; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.hsuyeung.blog.service.IRbacAuthorityService; 5 | import com.hsuyeung.blog.web.core.IBaseWebResponse; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.web.servlet.HandlerInterceptor; 10 | 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | 14 | import static org.springframework.http.HttpStatus.FORBIDDEN; 15 | 16 | /** 17 | * 用户权限检查拦截器 18 | * 19 | * @author hsuyeung 20 | * @date 2022/06/28 21 | */ 22 | @Slf4j 23 | @Component 24 | @RequiredArgsConstructor 25 | public class UserPermissionCheckInterceptor implements HandlerInterceptor, IBaseWebResponse { 26 | private final IRbacAuthorityService rbacAuthorityService; 27 | private final ObjectMapper objectMapper; 28 | 29 | @Override 30 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 31 | boolean hasPermission = rbacAuthorityService.hasPermission(request); 32 | if (!hasPermission) { 33 | response.setCharacterEncoding("UTF-8"); 34 | response.setContentType("application/json; charset=UTF-8"); 35 | response.getWriter().write(objectMapper.writeValueAsString(err(FORBIDDEN.value(), "没有权限操作该资源"))); 36 | return false; 37 | } 38 | return HandlerInterceptor.super.preHandle(request, response, handler); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/resources/static/css/error.min.css: -------------------------------------------------------------------------------- 1 | .icon{-webkit-user-select:none;user-select:none;display:inline-block}.icon-offline{content:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABIAQMAAABvIyEEAAAABlBMVEUAAABTU1OoaSf/AAAAAXRSTlMAQObYZgAAAGxJREFUeF7tyMEJwkAQRuFf5ipMKxYQiJ3Z2nSwrWwBA0+DQZcdxEOueaePp9+dQZFB7GpUcURSVU66yVNFj6LFICatThZB6r/ko/pbRpUgilY0Cbw5sNmb9txGXUKyuH7eV25x39DtJXUNPQGJtWFV+BT/QAAAAABJRU5ErkJggg==);position:relative}.offline .interstitial-wrapper{color:#2b2b2b;font-size:1em;line-height:1.55;margin:0 auto;max-width:600px;padding-top:100px;width:100%}#offline-resources{display:none}#message h1{font-size:34px;color:#555}#message p{color:#555;padding:5px 0;line-height:36px;letter-spacing:1px;font-size:18px}#message p a,p a:hover{color:#777;padding:0 8px}#message p span{color:#777;margin:0 8px;padding:1px 2px;background:#eee;border-radius:.3em}@media (max-width:736px){#message h1{margin:0 20px 0 20px}#message p{padding:20px}}@media (max-width:420px){#message h1{font-size:22px}}@media (max-height:350px){h1{margin:0 0 15px}.icon-offline{margin:0 0 10px}.interstitial-wrapper{margin-top:5%}}@media (min-width:600px) and (max-width:736px) and (orientation:landscape){.offline .interstitial-wrapper{margin-left:0;margin-right:0}}@media (min-width:420px) and (max-width:736px) and (min-height:240px) and (max-height:420px) and (orientation:landscape){.interstitial-wrapper{margin-bottom:100px}}@media (min-height:240px) and (orientation:landscape){.offline .interstitial-wrapper{margin-bottom:90px}.icon-offline{margin-bottom:20px}}@media (max-height:320px) and (orientation:landscape){.icon-offline{margin-bottom:0}}@media (max-width:240px){.interstitial-wrapper{overflow:inherit;padding:0 8px}} 2 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/cache/CommentCache.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.cache; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.core.type.TypeReference; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.hsuyeung.blog.exception.SystemInternalException; 7 | import com.hsuyeung.blog.model.entity.CommentEntity; 8 | import com.hsuyeung.blog.util.RedisUtil; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * @author hsuyeung 16 | * @date 2022/06/18 17 | */ 18 | @Component 19 | @RequiredArgsConstructor 20 | public class CommentCache { 21 | private final RedisUtil redisUtil; 22 | private final ObjectMapper objectMapper; 23 | 24 | 25 | public List getCommentList(String key, Long articleId) { 26 | String value = (String) redisUtil.hGet(key, String.valueOf(articleId)); 27 | try { 28 | return value == null ? null : objectMapper.readValue(value, new TypeReference>() { 29 | }); 30 | } catch (JsonProcessingException e) { 31 | throw new SystemInternalException("反序列化 CommentEntity 对象失败", e); 32 | } 33 | } 34 | 35 | public void cacheCommentList(String key, Long articleId, List commentEntityList) { 36 | try { 37 | redisUtil.hPut(key, String.valueOf(articleId), objectMapper.writeValueAsString(commentEntityList)); 38 | } catch (JsonProcessingException e) { 39 | throw new SystemInternalException("序列化 CommentEntity 对象失败", e); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/constant/RegexConstants.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.constant; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | /** 6 | * @author hsuyeung 7 | * @date 2022/06/17 8 | */ 9 | public final class RegexConstants { 10 | public static final String WEBSITE_PREFIX_REGEX = "^(http:\\/\\/|https:\\/\\/)([\\w.]+\\/?)\\S*$"; 11 | public static final String EMAIL_REGEX = "^([A-Za-z\\d_\\-\\.])+\\@([A-Za-z\\d_\\-\\.])+\\.([A-Za-z]{2,8})$"; 12 | public static final String JAVASCRIPT_TAG_REGEX = "]*?>[\\s\\S]*?<\\/script>"; 13 | public static final Pattern JAVASCRIPT_TAG_PATTERN = Pattern.compile(JAVASCRIPT_TAG_REGEX, Pattern.CASE_INSENSITIVE); 14 | public static final String STYLE_TAG_REGEX = "]*?>[\\s\\S]*?<\\/style>"; 15 | public static final Pattern STYLE_TAG_PATTERN = Pattern.compile(STYLE_TAG_REGEX, Pattern.CASE_INSENSITIVE); 16 | public static final String HTML_TAG_REGEX = "<[^>]+>"; 17 | public static final Pattern HTML_TAG_PATTERN = Pattern.compile(HTML_TAG_REGEX, Pattern.CASE_INSENSITIVE); 18 | public static final Pattern SPACE_TAG_PATTERN = Pattern.compile("\\s+|\t|\r|\n", Pattern.CASE_INSENSITIVE); 19 | 20 | public static final String ALL_NUMBER_REGEX = "^\\d+$"; 21 | public static final String USERNAME_REGEX = "^[a-zA-Z]\\w*$"; 22 | public static final String ROLE_CODE_REGEX = "^[a-zA-Z]\\w*$"; 23 | public static final String NICKNAME_REGEX = "^[a-zA-Z\\d\\u4e00-\\u9fa5]+$"; 24 | public static final String PASSWORD_REGEX = "^[a-zA-Z][a-zA-Z_!@%\\d]{7,15}$"; 25 | public static final String HTTP_METHOD_REGEX = "^(?i)(get|put|post|delete)$"; 26 | 27 | private RegexConstants() { 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/service/impl/ContentServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.service.impl; 2 | 3 | import com.baomidou.mybatisplus.core.toolkit.support.SFunction; 4 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 5 | import com.hsuyeung.blog.constant.enums.ContentTypeEnum; 6 | import com.hsuyeung.blog.mapper.ContentMapper; 7 | import com.hsuyeung.blog.model.entity.ContentEntity; 8 | import com.hsuyeung.blog.service.IContentService; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.Objects; 13 | 14 | import static com.hsuyeung.blog.constant.enums.ContentTypeEnum.HTML; 15 | 16 | /** 17 | *

18 | * 文章内容表 服务实现类 19 | *

20 | * 21 | * @author hsuyeung 22 | * @since 2022/06/05 23 | */ 24 | @Slf4j 25 | @Service 26 | public class ContentServiceImpl extends ServiceImpl implements IContentService { 27 | 28 | @Override 29 | public String getContent(Long contentId, ContentTypeEnum contentType) { 30 | boolean isHtml = Objects.equals(contentType, HTML); 31 | SFunction contentColumn = isHtml 32 | ? ContentEntity::getHtmlContent 33 | : ContentEntity::getMdContent; 34 | ContentEntity contentEntity = lambdaQuery() 35 | .select(contentColumn) 36 | .eq(ContentEntity::getId, contentId) 37 | .one(); 38 | if (Objects.isNull(contentEntity)) { 39 | log.error(String.format("id 为 %d 的内容不存在", contentId)); 40 | return ""; 41 | } 42 | return isHtml ? contentEntity.getHtmlContent() : contentEntity.getMdContent(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/comment/SubmitCommentDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.comment; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotNull; 8 | import javax.validation.constraints.Size; 9 | import java.io.Serializable; 10 | 11 | /** 12 | * 提交评论请求参数实体类 13 | * 14 | * @author hsuyeung 15 | * @date 2022/06/16 16 | */ 17 | @ApiModel(description = "提交评论请求参数") 18 | @Data 19 | public class SubmitCommentDTO implements Serializable { 20 | private static final long serialVersionUID = 2168326647375039892L; 21 | 22 | @ApiModelProperty(value = "文章 id", required = true) 23 | @NotNull(message = "文章 id 不能为 null") 24 | private Long articleId; 25 | 26 | @ApiModelProperty("是否接收回复提醒邮件") 27 | private Boolean isNotificationChecked; 28 | 29 | @ApiModelProperty(value = "一级评论 id", required = true) 30 | @NotNull(message = "父级评论 id 不能为 null") 31 | private Long parentCommentId; 32 | 33 | @ApiModelProperty(value = "直接回复的评论 id", required = true) 34 | @NotNull(message = "回复的评论 id 不能为 null") 35 | private Long replyCommentId; 36 | 37 | @ApiModelProperty(value = "评论内容", required = true) 38 | @Size(min = 1, message = "请输入评论内容") 39 | private String content; 40 | 41 | @ApiModelProperty(value = "昵称", required = true) 42 | @Size(min = 1, max = 8, message = "昵称长度限制区间为 (0, 8] 个字符") 43 | private String nickname; 44 | 45 | @ApiModelProperty(value = "邮箱地址", required = true) 46 | @Size(max = 64, message = "邮箱地址长度不能超过 64 个字符") 47 | private String email; 48 | 49 | @ApiModelProperty("网址") 50 | private String website; 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/comment/SubmitCommentRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.comment; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotNull; 8 | import javax.validation.constraints.Size; 9 | import java.io.Serializable; 10 | 11 | /** 12 | * 提交评论请求参数实体类 13 | * 14 | * @author hsuyeung 15 | * @date 2022/06/16 16 | */ 17 | @ApiModel(description = "提交评论请求参数") 18 | @Data 19 | public class SubmitCommentRequestDTO implements Serializable { 20 | private static final long serialVersionUID = 2168326647375039892L; 21 | 22 | @ApiModelProperty(value = "文章 id", required = true) 23 | @NotNull(message = "文章 id 不能为空") 24 | private Long articleId; 25 | 26 | @ApiModelProperty("是否接收回复提醒邮件") 27 | private Boolean isNotificationChecked; 28 | 29 | @ApiModelProperty(value = "一级评论 id", required = true) 30 | @NotNull(message = "父级评论 id 不能为空") 31 | private Long parentCommentId; 32 | 33 | @ApiModelProperty(value = "直接回复的评论 id", required = true) 34 | @NotNull(message = "回复的评论 id 不能为空") 35 | private Long replyCommentId; 36 | 37 | @ApiModelProperty(value = "评论内容", required = true) 38 | @Size(min = 1, message = "请输入评论内容") 39 | private String content; 40 | 41 | @ApiModelProperty(value = "昵称", required = true) 42 | @Size(min = 1, max = 8, message = "昵称长度限制区间为 (0, 8] 个字符") 43 | private String nickname; 44 | 45 | @ApiModelProperty(value = "邮箱地址", required = true) 46 | @Size(max = 64, message = "邮箱地址长度不能超过 64 个字符") 47 | private String email; 48 | 49 | @ApiModelProperty("网址") 50 | private String website; 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/entity/ArticleEntity.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.TableField; 4 | import com.baomidou.mybatisplus.annotation.TableName; 5 | import com.hsuyeung.blog.constant.enums.PinEnum; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.NoArgsConstructor; 10 | import lombok.experimental.Accessors; 11 | import lombok.experimental.SuperBuilder; 12 | 13 | /** 14 | *

15 | * 博客文章表 16 | *

17 | * 18 | * @author hsuyeung 19 | * @since 2022/06/05 20 | */ 21 | @Data 22 | @EqualsAndHashCode(callSuper = true) 23 | @SuperBuilder 24 | @NoArgsConstructor 25 | @AllArgsConstructor 26 | @Accessors(chain = true) 27 | @TableName("t_article") 28 | public class ArticleEntity extends BaseEntity { 29 | private static final long serialVersionUID = 4362214654700368692L; 30 | 31 | /** 32 | * 文章标题 33 | */ 34 | @TableField("title") 35 | private String title; 36 | 37 | /** 38 | * 文章路由 39 | */ 40 | @TableField("route") 41 | private String route; 42 | 43 | /** 44 | * 文章作者 45 | */ 46 | @TableField("author") 47 | private String author; 48 | 49 | /** 50 | * 文章关键词,多个用逗号分隔 51 | */ 52 | @TableField("keywords") 53 | private String keywords; 54 | 55 | /** 56 | * 文章描述 57 | */ 58 | @TableField("description") 59 | private String description; 60 | 61 | /** 62 | * 内容 id 63 | */ 64 | @TableField("content_id") 65 | private Long contentId; 66 | 67 | /** 68 | * 是否置顶(0: 不置顶;1: 置顶) 69 | */ 70 | @TableField("is_pin") 71 | private PinEnum pin; 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/entity/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.*; 4 | import com.hsuyeung.blog.constant.enums.LogicDeleteEnum; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.NoArgsConstructor; 9 | import lombok.experimental.SuperBuilder; 10 | 11 | import java.io.Serializable; 12 | import java.time.LocalDateTime; 13 | 14 | /** 15 | * 数据库表公共字段实体类 16 | * 17 | * @author hsuyeung 18 | * @date 2022/06/05 19 | */ 20 | @Data 21 | @SuperBuilder 22 | @NoArgsConstructor 23 | @AllArgsConstructor 24 | @EqualsAndHashCode 25 | public abstract class BaseEntity implements Serializable { 26 | private static final long serialVersionUID = -2098692285088879059L; 27 | 28 | /** 29 | * 主键 id,自增 30 | */ 31 | @TableId(value = "id", type = IdType.AUTO) 32 | private Long id; 33 | 34 | /** 35 | * 创建者 36 | */ 37 | @TableField(value = "create_by", fill = FieldFill.INSERT) 38 | private String createBy; 39 | 40 | /** 41 | * 创建时间 42 | */ 43 | @TableField(value = "create_time", fill = FieldFill.INSERT) 44 | private LocalDateTime createTime; 45 | 46 | /** 47 | * 更新人 48 | */ 49 | @TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE) 50 | private String updateBy; 51 | 52 | /** 53 | * 更新时间 54 | */ 55 | @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE) 56 | private LocalDateTime updateTime; 57 | 58 | /** 59 | * 逻辑删除字段 60 | */ 61 | @TableLogic 62 | @TableField(value = "is_deleted", fill = FieldFill.INSERT) 63 | private LogicDeleteEnum deleted; 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/filter/AddHeaderFilter.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.filter; 2 | 3 | import com.hsuyeung.blog.config.properties.SecurityProperties; 4 | import com.hsuyeung.blog.web.core.HeaderMapRequestWrapper; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.util.StringUtils; 8 | 9 | import javax.servlet.*; 10 | import javax.servlet.http.HttpServletRequest; 11 | import java.io.IOException; 12 | 13 | /** 14 | * @author hsuyeung 15 | * @date 2022/12/23 16 | */ 17 | @Component 18 | @RequiredArgsConstructor 19 | public class AddHeaderFilter implements Filter { 20 | private final SecurityProperties properties; 21 | 22 | @Override 23 | public void init(FilterConfig filterConfig) throws ServletException { 24 | Filter.super.init(filterConfig); 25 | } 26 | 27 | @Override 28 | public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) 29 | throws IOException, ServletException { 30 | HttpServletRequest req = (HttpServletRequest) servletRequest; 31 | String tokenName = properties.getTokenName(); 32 | String token = req.getParameter(tokenName); 33 | if (StringUtils.hasText(token)) { 34 | HeaderMapRequestWrapper requestWrapper = new HeaderMapRequestWrapper(req); 35 | requestWrapper.addHeader(tokenName, token); 36 | filterChain.doFilter(requestWrapper, servletResponse); 37 | } else { 38 | filterChain.doFilter(servletRequest, servletResponse); 39 | } 40 | } 41 | 42 | @Override 43 | public void destroy() { 44 | Filter.super.destroy(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/user/UpdateUserDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.user; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import javax.validation.constraints.Size; 10 | import java.io.Serializable; 11 | 12 | /** 13 | * 更新用户信息请求参数 14 | * 15 | * @author hsuyeung 16 | * @date 2022/06/29 17 | */ 18 | @ApiModel(description = "更新用户信息请求参数") 19 | @Data 20 | public class UpdateUserDTO implements Serializable { 21 | private static final long serialVersionUID = -6710749659912700292L; 22 | 23 | @ApiModelProperty(value = "用户 id", required = true) 24 | @NotNull(message = "用户 id 不能为 null") 25 | private Long id; 26 | 27 | @ApiModelProperty(value = "用户名", required = true) 28 | @NotBlank(message = "用户名不能为空") 29 | @Size(max = 32, message = "用户名不能超过 32 个字符") 30 | private String username; 31 | 32 | @ApiModelProperty("旧密码") 33 | @Size(max = 16, message = "密码不能超过 16 个字符") 34 | private String oldPassword; 35 | 36 | @ApiModelProperty("新密码") 37 | @Size(max = 16, message = "密码不能超过 16 个字符") 38 | private String newPassword; 39 | 40 | @ApiModelProperty("再次确认新密码") 41 | @Size(max = 16, message = "密码不能超过 16 个字符") 42 | private String reconfirmNewPassword; 43 | 44 | @ApiModelProperty(value = "昵称", required = true) 45 | @NotBlank(message = "用户昵称不能为空") 46 | @Size(max = 64, message = "昵称不能超过 64 个字符") 47 | private String nickname; 48 | 49 | @ApiModelProperty(value = "是否可用", required = true) 50 | @NotNull(message = "是否可用不能为空") 51 | private Boolean enabled; 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/hsuyeung/blog/model/dto/friendlink/UpdateFriendLinkDTO.java: -------------------------------------------------------------------------------- 1 | package com.hsuyeung.blog.model.dto.friendlink; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import javax.validation.constraints.Size; 10 | import java.io.Serializable; 11 | 12 | /** 13 | * 更新友链请求参数实体类 14 | * 15 | * @author hsuyeung 16 | * @date 2022/06/25 17 | */ 18 | @ApiModel(description = "更新友链请求参数") 19 | @Data 20 | public class UpdateFriendLinkDTO implements Serializable { 21 | private static final long serialVersionUID = 7903987898815576207L; 22 | 23 | @ApiModelProperty(value = "友链 id", required = true) 24 | @NotNull(message = "id 不能为 null") 25 | private Long id; 26 | 27 | @ApiModelProperty(value = "友链名称", required = true) 28 | @NotBlank(message = "友链名称不能为空") 29 | @Size(max = 255, message = "友链名称不能超过 255 个字符") 30 | private String linkName; 31 | 32 | @ApiModelProperty(value = "友链链接", required = true) 33 | @NotBlank(message = "友链链接不能为空") 34 | @Size(max = 255, message = "友链链接不能超过 255 个字符") 35 | private String linkUrl; 36 | 37 | @ApiModelProperty(value = "友链头像", required = true) 38 | @NotBlank(message = "友链头像不能为空") 39 | @Size(max = 255, message = "友链头像不能超过 255 个字符") 40 | private String linkAvatar; 41 | 42 | @ApiModelProperty(value = "一句话描述", required = true) 43 | @Size(max = 255, message = "一句话描述不能超过 255 个字符") 44 | private String linkDesc; 45 | 46 | @ApiModelProperty(value = "友链分组", required = true) 47 | @NotBlank(message = "友链分组不能为空") 48 | @Size(max = 255, message = "友链分组不能超过 255 个字符") 49 | private String linkGroup; 50 | } 51 | --------------------------------------------------------------------------------