8 | * 博客文章表 Mapper 接口 9 | *
10 | * 11 | * @author hsuyeung 12 | * @since 2022/06/05 13 | */ 14 | public interface ArticleMapper extends BaseMapper8 | * 文章内容表 Mapper 接口 9 | *
10 | * 11 | * @author hsuyeung 12 | * @since 2022/06/05 13 | */ 14 | public interface ContentMapper extends BaseMapper9 | * 文章评论 Mapper 接口 10 | *
11 | * 12 | * @author hsuyeung 13 | * @since 2022/06/05 14 | */ 15 | public interface CommentMapper extends BaseMapper8 | * 系统配置表 Mapper 接口 9 | *
10 | * 11 | * @author hsuyeung 12 | * @since 2022/06/05 13 | */ 14 | public interface SystemConfigMapper extends BaseMapper
这种异常不应该返回给客户端展示,统一返回系统繁忙并记录异常日志
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该异常是可以返回给客户端展示的信息
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 |这一个结点即表示一个缓存数据
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 |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 |
目前的登录策略:每访问一次登录接口就将之前的 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 | 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
评论管理
4 |