前后端分离博客项目
使用技术栈
SpringBoot,MybatisPlus,SpringSecurity,EasyExcel,Swagger2,Redis…
环境搭建
创建工程
多模块项目
博客项目的前台和后台都有相同的用户,文章等概念,如果分为两个独立的工程那么出现的重复代码会很多,代码复用性不高
所以我们要创建多模块项目,两套系统可能都会用到的代码写到一个公共模块中,让前台系统和后台系统分别依赖公共模块
此项目我们使用的是三个模块
- os467-framework:存放前台后台共同需要的业务与实体
- os467-front:前台的控制器,独立业务
- os467-admin:后台的控制器,以及独立业务
创建父模块
idea选择创建pom工程,删除此模块下的src目录
配置父pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.os467</groupId>
<artifactId>os467-blog</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<!--子模块依赖版本控制,锁定子模块依赖版本-->
<dependencyManagement>
<dependencies>
<!-- SpringBoot的依赖配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.5.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--fastjson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!--mybatisPlus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<!--阿里云OSS-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.11.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
创建公共子模块
os467-framework
子模块依赖添加,继承自父工程
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--junit-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--SpringSecurity启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--fastjson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
<!--mybatisPlus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!--mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--阿里云OSS-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<!--AOP-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
</dependencies>
为前台和后台子模块添加坐标
前台
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>os467-blog</artifactId>
<groupId>com.os467</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>os467-front</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.os467</groupId>
<artifactId>os467-framework</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
后台
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>os467-blog</artifactId>
<groupId>com.os467</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>os467-admin</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.os467</groupId>
<artifactId>os467-framework</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
创建启动类
前台启动类创建
@SpringBootApplication
public class Os467FrontApplication {
public static void main(String[] args) {
SpringApplication.run(Os467FrontApplication.class,args);
}
}
配置文件
server:
port: 7777
spring:
datasource:
url: jdbc:mysql://localhost:3306/sg_blog?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
servlet:
multipart:
max-file-size: 2MB
max-request-size: 5MB
mybatis-plus:
configuration:
# 日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: delFlag
logic-delete-value: 1
logic-not-delete-value: 0
id-type: auto
IDEA插件自动生成实体类
1、使用插件Easy Code
2、使用IDEA自动生成数据库表对应实体类
详细请参考博客:https://blog.csdn.net/qq_34371461/article/details/80571281
自定义生成器
如果不需要对表名特殊处理,请将此代码中调用的javaClassName
方法改为javaName
方法
注释使用mybatisPlus的注释,如果有其它需要可以自行修改
import com.intellij.database.model.DasTable
import com.intellij.database.model.ObjectKind
import com.intellij.database.util.Case
import com.intellij.database.util.DasUtil
import java.io.*
import java.text.SimpleDateFormat
/*
* Available context bindings:
* SELECTION Iterable<DasObject>
* PROJECT project
* FILES files helper
*/
packageName = ""
typeMapping = [
(~/(?i)tinyint|smallint|mediumint/) : "Integer",
(~/(?i)int/) : "Long",
(~/(?i)bool|bit/) : "Boolean",
(~/(?i)float|double|decimal|real/) : "Double",
(~/(?i)datetime|timestamp|date|time/) : "Date",
(~/(?i)blob|binary|bfile|clob|raw|image/): "InputStream",
(~/(?i)/) : "String"
]
FILES.chooseDirectoryAndSave("Choose directory", "Choose where to store generated files") { dir ->
SELECTION.filter { it instanceof DasTable && it.getKind() == ObjectKind.TABLE }.each { generate(it, dir) }
}
def generate(table, dir) {
def className = javaName(table.getName(), true)
def fields = calcFields(table)
packageName = getPackageName(dir)
PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(new FileOutputStream(new File(dir, className + ".java")), "UTF-8"))
printWriter.withPrintWriter {out -> generate(out, className, fields,table)}
// new File(dir, className + ".java").withPrintWriter { out -> generate(out, className, fields,table) }
}
// 获取包所在文件夹路径
def getPackageName(dir) {
return dir.toString().replaceAll("\\\\", ".").replaceAll("/", ".").replaceAll("^.*src(\\.main\\.java\\.)?", "") + ";"
}
def generate(out, className, fields,table) {
out.println "package $packageName"
out.println ""
out.println "import com.baomidou.mybatisplus.annotation.TableField;"
out.println "import com.baomidou.mybatisplus.annotation.TableName;"
out.println "import java.io.Serializable;"
out.println "import lombok.Getter;"
out.println "import lombok.Setter;"
out.println "import lombok.ToString;"
Set types = new HashSet()
fields.each() {
types.add(it.type)
}
if (types.contains("Date")) {
out.println "import java.util.Date;"
}
if (types.contains("InputStream")) {
out.println "import java.io.InputStream;"
}
out.println ""
out.println "/**\n" +
" * @Description \n" +
" * @Author os467\n" +
" * @Date "+ new SimpleDateFormat("yyyy-MM-dd").format(new Date()) + " \n" +
" */"
out.println ""
out.println "@Setter"
out.println "@Getter"
out.println "@ToString"
out.println "@TableName ( value =\""+table.getName() +"\" )"
out.println "public class $className implements Serializable {"
out.println ""
out.println genSerialID()
fields.each() {
out.println ""
// 输出注释
if (isNotEmpty(it.commoent)) {
out.println "\t/**"
out.println "\t * ${it.commoent.toString()}"
out.println "\t */"
}
if (it.annos != "") out.println " ${it.annos.replace("[@Id]", "")}"
// 输出成员变量
out.println "\tprivate ${it.type} ${it.name};"
}
// 输出get/set方法
// fields.each() {
// out.println ""
// out.println "\tpublic ${it.type} get${it.name.capitalize()}() {"
// out.println "\t\treturn this.${it.name};"
// out.println "\t}"
// out.println ""
//
// out.println "\tpublic void set${it.name.capitalize()}(${it.type} ${it.name}) {"
// out.println "\t\tthis.${it.name} = ${it.name};"
// out.println "\t}"
// }
out.println ""
out.println "}"
}
def calcFields(table) {
DasUtil.getColumns(table).reduce([]) { fields, col ->
def spec = Case.LOWER.apply(col.getDataType().getSpecification())
def typeStr = typeMapping.find { p, t -> p.matcher(spec).find() }.value
def comm =[
colName : col.getName(),
name : javaName(col.getName(), false),
type : typeStr,
commoent: col.getComment(),
annos: "\t@TableField(value = \""+col.getName()+"\" )"]
if("id".equals(Case.LOWER.apply(col.getName())))
comm.annos +=["@Id"]
fields += [comm]
}
}
// 处理类名(这里是因为我的表都是以t_命名的,所以需要处理去掉生成类名时的开头的T,
// 如果你不需要那么请查找用到了 javaClassName这个方法的地方修改为 javaName 即可)
def javaClassName(str, capitalize) {
def s = com.intellij.psi.codeStyle.NameUtil.splitNameIntoWords(str)
.collect { Case.LOWER.apply(it).capitalize() }
.join("")
.replaceAll(/[^\p{javaJavaIdentifierPart}[_]]/, "_")
// 去除开头的T http://developer.51cto.com/art/200906/129168.htm
s = s[1..s.size() - 1]
capitalize || s.length() == 1? s : Case.LOWER.apply(s[0]) + s[1..-1]
}
def javaName(str, capitalize) {
// def s = str.split(/(?<=[^\p{IsLetter}])/).collect { Case.LOWER.apply(it).capitalize() }
// .join("").replaceAll(/[^\p{javaJavaIdentifierPart}]/, "_")
// capitalize || s.length() == 1? s : Case.LOWER.apply(s[0]) + s[1..-1]
def s = com.intellij.psi.codeStyle.NameUtil.splitNameIntoWords(str)
.collect { Case.LOWER.apply(it).capitalize() }
.join("")
.replaceAll(/[^\p{javaJavaIdentifierPart}[_]]/, "_")
capitalize || s.length() == 1? s : Case.LOWER.apply(s[0]) + s[1..-1]
}
def isNotEmpty(content) {
return content != null && content.toString().trim().length() > 0
}
static String changeStyle(String str, boolean toCamel){
if(!str || str.size() <= 1)
return str
if(toCamel){
String r = str.toLowerCase().split('_').collect{cc -> Case.LOWER.apply(cc).capitalize()}.join('')
return r[0].toLowerCase() + r[1..-1]
}else{
str = str[0].toLowerCase() + str[1..-1]
return str.collect{cc -> ((char)cc).isUpperCase() ? '_' + cc.toLowerCase() : cc}.join('')
}
}
static String genSerialID()
{
return "\tprivate static final long serialVersionUID = "+Math.abs(new Random().nextLong())+"L;"
}
连接数据库并查询
1、创建Article实体类
2、创建对应的Mapper和Service
==问题1:==在多模块项目中,我们可能会互相引用不同模块之间的类,比如使用
@Autowired
注解时,我们需要保证其它模块的类在修改后使用maven的clean+install命令重新打包==问题2:==我们只需要在一个模块的启动类上加
@MapperScan
注解就能扫描到其它模块下对应的mapper包中的Mapper接口例如在前台模块中需要用到通用模块的Mapper接口
通用类
响应类
package com.os467.common;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.os467.enums.AppHttpCodeEnum;
import java.io.Serializable;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> implements Serializable {
private Integer code;
private String msg;
private T data;
public ResponseResult() {
this.code = AppHttpCodeEnum.SUCCESS.getCode();
this.msg = AppHttpCodeEnum.SUCCESS.getMsg();
}
public ResponseResult(Integer code, T data) {
this.code = code;
this.data = data;
}
public ResponseResult(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public ResponseResult(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public static ResponseResult errorResult(int code, String msg) {
ResponseResult result = new ResponseResult();
return result.error(code, msg);
}
public static ResponseResult okResult() {
ResponseResult result = new ResponseResult();
return result;
}
public static ResponseResult okResult(int code, String msg) {
ResponseResult result = new ResponseResult();
return result.ok(code, null, msg);
}
public static ResponseResult okResult(Object data) {
ResponseResult result = setAppHttpCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getMsg());
if(data!=null) {
result.setData(data);
}
return result;
}
public static ResponseResult errorResult(AppHttpCodeEnum enums){
return setAppHttpCodeEnum(enums,enums.getMsg());
}
public static ResponseResult errorResult(AppHttpCodeEnum enums, String msg){
return setAppHttpCodeEnum(enums,msg);
}
public static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums){
return okResult(enums.getCode(),enums.getMsg());
}
private static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums, String msg){
return okResult(enums.getCode(),msg);
}
public ResponseResult<?> error(Integer code, String msg) {
this.code = code;
this.msg = msg;
return this;
}
public ResponseResult<?> ok(Integer code, T data) {
this.code = code;
this.data = data;
return this;
}
public ResponseResult<?> ok(Integer code, T data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
return this;
}
public ResponseResult<?> ok(T data) {
this.data = data;
return this;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
响应枚举类
package com.os467.enums;
public enum AppHttpCodeEnum {
// 成功
SUCCESS(200,"操作成功"),
// 登录
NEED_LOGIN(401,"需要登录后操作"),
NO_OPERATOR_AUTH(403,"无权限操作"),
SYSTEM_ERROR(500,"出现错误"),
USERNAME_EXIST(501,"用户名已存在"),
PHONENUMBER_EXIST(502,"手机号已存在"), EMAIL_EXIST(503, "邮箱已存在"),
REQUIRE_USERNAME(504, "必需填写用户名"),
LOGIN_ERROR(505,"用户名或密码错误");
int code;
String msg;
AppHttpCodeEnum(int code, String errorMessage){
this.code = code;
this.msg = errorMessage;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
1.热门文章列表
通过需求分析需要哪些字段
需要查询浏览量最高的前10篇文章的信息,要求展示文章标题和浏览量,把能让用户自己点击跳转到具体的文章详情进行浏览
不能把草稿展示出来,不能把删除了的文章查询出来,要按照浏览量进行降序排序
*解决跨域访问问题
spring允许跨域访问
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
SpringSecurity允许跨域访问
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().disable();
}
}
实体类
我们在查询文章类时,如果只需要某些字段,这时候我们可以使用vo类来封装文章类的部分字段
vo类被设计出来只是为了封装提供给接口所需要的数据,一个接口对应一个vo,因此如果后期接口响应字段要修改,我们也只需要修改vo即可
文章类
package com.os467.entity;
import java.util.Date;
import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 文章表(Article)表实体类
*
* @author makejava
* @since 2022-09-24 16:52:44
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuppressWarnings("serial")
public class Article {
private Long id;
//标题
private String title;
//文章内容
private String content;
//文章摘要
private String summary;
//所属分类id
private Long categoryId;
//缩略图
private String thumbnail;
//是否置顶(0否,1是)
private String isTop;
//状态(0已发布,1草稿)
private String status;
//访问量
private Long viewCount;
//是否允许评论 1是,0否
private String isComment;
private Long createBy;
private Date createTime;
private Long updateBy;
private Date updateTime;
//删除标志(0代表未删除,1代表已删除)
private Integer delFlag;
}
热门文章数据类
package com.os467.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class HotArticleVo {
/**
* 需要获得id
*/
private Long id;
/**
* 标题
*/
private String title;
/**
* 访问量
*/
private Long viewCount;
}
表现层
@RestController
@RequestMapping("/article")
public class ArticleController {
@Autowired
ArticleService articleService;
@GetMapping("/hotArticleList")
public ResponseResult getHotArticleList(){
return articleService.getHotArticleList();
}
}
业务逻辑
public ResponseResult getHotArticleList() {
//构造查询
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
//查询条件,根据访问量降序排序
queryWrapper.orderByDesc(Article::getViewCount);
//不能将草稿查询出来,只查询status为0的
queryWrapper.eq(Article::getStatus,0);
//不能将已被删除的查询出来,只查询deleteFlag为0的
queryWrapper.eq(Article::getDelFlag,0);
//构造分页工具
Page<Article> articlePage = new Page<>(1,10);
//分页查询
articleMapper.selectPage(articlePage, queryWrapper);
//获取分页查询后的数据
List<Article> articles = articlePage.getRecords();
//存放vo数据的数组
List<HotArticleVo> articleVos = new ArrayList<>();
//for循环拷贝
for (Article article : articles) {
//创建vo对象
HotArticleVo vo = new HotArticleVo();
//使用实体字段拷贝
BeanUtils.copyProperties(article,vo);
//将vo对象添加进vo数组中
articleVos.add(vo);
}
//返回封装好的response对象
return ResponseResult.okResult(articleVos);
}
*字面值处理
实际项目中都不允许直接在代码中使用字面值,都需要定义成常量来使用,这种方式有利于提高代码的可维护性
package com.os467.common;
public class SystemConstants {
public static final String ARTICLE_STATUS_DRAFT = "1";
public static final String ARTICLE_STATUS_NORMAL = "0";
public static final int DELETE_FLAG_TRUE = 1;
public static final int DELETE_FLAG_FALSE = 0;
}
替换刚刚使用的字面量值
//不能将草稿查询出来,只查询status为0的
queryWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);
//不能将已被删除的查询出来,只查询deleteFlag为0的
queryWrapper.eq(Article::getDelFlag,SystemConstants.DELETE_FLAG_FALSE);
2.查询分类列表
需求:
页面上需要展示分类列表,用户可以点击具体的分类查看该分类下的文章列表
注意: ①要求只展示有发布正式文章的分类
②必须是正常状态的分类
查询文章表,找到文章表对应的类id,获取类id列表装换为set集合
*实体拷贝工具类
==踩坑:使用拷贝工具必须要为两个资源都提供set和get方法!!==
public class BeanCopyUtils {
private BeanCopyUtils(){}
public static <V> V copyBean(Object source,Class<V> clazz){
//创建目标对象
V result = null;
try {
result = clazz.newInstance();
BeanUtils.copyProperties(source,result);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return result;
}
/**
* 拷贝列表内所有对象
*/
public static <O,V> List<V> copyBeanList(List<O> list,Class<V> clazz){
return list.stream().map(o ->
copyBean(o,clazz)
).collect(Collectors.toList());
}
}
实体类
package com.os467.entity;
import java.util.Date;
import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 分类表(Os467Category)表实体类
*
* @author makejava
* @since 2022-09-25 17:58:51
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuppressWarnings("serial")
public class Category {
private Long id;
//分类名
private String name;
//父分类id,如果没有父分类为-1
private Long pid;
//描述
private String description;
//状态0:正常,1禁用
private String status;
private Long createBy;
private Date createTime;
private Long updateBy;
private Date updateTime;
//删除标志(0代表未删除,1代表已删除)
private Integer delFlag;
表现层
@RestController
@RequestMapping("/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
@GetMapping("/getCategoryList")
public ResponseResult getCategoryList(){
return categoryService.getCategoryList();
}
}
业务逻辑
/**
* 获取到分类列表,并且此分类下必须有文章
* @return
*/
@Override
public ResponseResult getCategoryList() {
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
//查询categoryId字段
//不能将草稿查询出来,只查询status为0的
queryWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);
//不能将已被删除的查询出来,只查询deleteFlag为0的
queryWrapper.eq(Article::getDelFlag,SystemConstants.DELETE_FLAG_FALSE);
//获取到信息
List<Article> articles = articleMapper.selectList(queryWrapper);
//将列表转换为集合,进行去重
Set<Long> categoryIds = articles.stream().map(Article::getCategoryId).collect(Collectors.toSet());
//获取查询到的分类结果
List<Category> categories = categoryMapper.selectBatchIds(categoryIds);
//筛选查询结果
List<Category> categoryList = categories.stream().filter(category -> {
//正常状态下的分类
return category.getStatus().equals(SystemConstants.ARTICLE_STATUS_NORMAL);
}).collect(Collectors.toList());
List<CategoryVo> categoryVos = BeanCopyUtils.copyBeanList(categoryList, CategoryVo.class);
//返回查询结果
return ResponseResult.okResult(categoryVos);
}
2.1根据分类id查询文章
在首页和分类页面都需要查询文章列表
首页:查询所有的文章
分类页面:查询对应分类下的文章
要求:①只能查询正式发布的文章 ②置顶的文章要显示在最前面
*MybatisPlus分页配置
package com.os467.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
/**
* 3.4.0之后版本
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
}
实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageVo {
private List rows;
private Long total;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ArticleListVo {
private Long id;
//标题
private String title;
//文章摘要
private String summary;
//所属分类名
private String categoryName;
//缩略图
private String thumbnail;
//访问量
private Long viewCount;
private Date createTime;
}
业务逻辑
还需要返回当前文章对应的分类信息
/**
* 根据文章分类获取文章内容
* @param pageNum
* @param pageSize
* @param categoryId
* @return
*/
@Override
public ResponseResult getArticleList(Integer pageNum, Integer pageSize, Long categoryId) {
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
//此分类下的非草稿,未删除文章
queryWrapper
.eq(Article::getStatus,SystemConstants.ARTICLE_STATUS_NORMAL);
//根据分类id来查询
queryWrapper.eq(Objects.nonNull(categoryId)&&categoryId > 0,Article::getCategoryId,categoryId);
//根据置顶文章字段降序排序
queryWrapper.orderByDesc(Article::getIsTop);
//分页查询构建
Page<Article> articlePage = new Page<>(pageNum,pageSize);
//查询
List<Article> articleList = articleMapper.selectPage(articlePage, queryWrapper).getRecords();
//调用分类字段查询,根据文章id获取分类名
articleList.stream().map(
article -> {
Category category = categoryMapper.selectById(article.getCategoryId());
article.setCategoryName(category.getName());
return article;
}
).collect(Collectors.toList());
//构建文章列表Vo对象
List<ArticleListVo> articleListVos = BeanCopyUtils.copyBeanList(articleList, ArticleListVo.class);
//构建pageVo对象
PageVo pageVo = new PageVo(articleListVos, articlePage.getTotal());
//生成响应结果对象
ResponseResult responseResult = ResponseResult.okResult(pageVo);
return responseResult;
}
*通用fastJson配置
配置在配置类中
com.alibaba.fastjson
的api
@Bean//使用@Bean注入fastJsonHttpMessageConvert
public HttpMessageConverter fastJsonHttpMessageConverters() {
//1.需要定义一个Convert转换消息的对象
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
SerializeConfig.globalInstance.put(Long.class, ToStringSerializer.instance);
fastJsonConfig.setSerializeConfig(SerializeConfig.globalInstance);
fastConverter.setFastJsonConfig(fastJsonConfig);
HttpMessageConverter<?> converter = fastConverter;
return converter;
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(fastJsonHttpMessageConverters());
}
3.文章详情
需求
要求在文章列表点击阅读全文时能够跳转到文章详情页面,可以让用户阅读文章正文。
要求:①要在文章详情中展示其分类名
请求方式 | 请求路径 |
---|---|
Get | /article/{id} |
业务逻辑
/**
* 根据文章id获取文章详情页信息
* @param id
* @return
*/
@Override
public ResponseResult getArticleDetail(Long id) {
//获得文章信息
Article article = articleMapper.selectById(id);
//查询分类信息
Category category = categoryMapper.selectById(article.getCategoryId());
//将分类信息存放到文章信息中
if (category != null){
article.setCategoryName(category.getName());
}
//拷贝文章信息到Vo上
ArticleDetailVo articleDetailVo = BeanCopyUtils.copyBean(article, ArticleDetailVo.class);
return ResponseResult.okResult(articleDetailVo);
}
4.友链功能
实体类
package com.os467.entity;
import java.util.Date;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 友链(Link)表实体类
*
* @author makejava
* @since 2022-09-27 22:06:20
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuppressWarnings("serial")
public class Link {
private Long id;
private String name;
private String logo;
private String description;
//网站地址
private String address;
//审核状态 (0代表审核通过,1代表审核未通过,2代表未审核)
private String status;
private Long createBy;
private Date createTime;
private Long updateBy;
private Date updateTime;
//删除标志(0代表未删除,1代表已删除)
private Integer delFlag;
}
package com.os467.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* 友链(Link)表实体类
*
* @author makejava
* @since 2022-09-27 22:06:20
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuppressWarnings("serial")
public class LinkVo {
private Long id;
private String name;
private String logo;
private String description;
//网站地址
private String address;
}
表现层
@Autowired
private CategoryService categoryService;
@GetMapping("/getCategoryList")
public ResponseResult getCategoryList(){
return categoryService.getCategoryList();
}
业务层
/**
* 获取到所有友链信息
* @return
*/
@Override
public ResponseResult getAllLink() {
LambdaQueryWrapper<Link> queryWrapper = new LambdaQueryWrapper<>();
//查询审核通过的
queryWrapper.eq(Link::getStatus, SystemConstants.LINK_STATUS_NORMAL);
List<Link> linkList = linkMapper.selectList(queryWrapper);
//封装成Vo对象
List<LinkVo> linkVos = BeanCopyUtils.copyBeanList(linkList, LinkVo.class);
ResponseResult responseResult = ResponseResult.okResult(linkVos);
return responseResult;
}
5.SpringSecurity实现登录认证
基本流程:
重写配置类configure逻辑,弃用UsernamePasswordAuthenticationFilter
过滤器
为login接口配置认证流程
配置认证管理器AuthenticationManager
用于提供后续的认证业务
重新配置账号解码器,改为使用BCryptPasswordEncoder
定义LoginUser实现UserDetails
接口
实现UserDetailsService
接口,从数据库中查询用户信息并且封装用户信息(授权信息)
返回封装好的UserDetails
信息后会调用解码器进行密码校验
认证成功后会在登录业务中调用AuthenticationManager
处返回用户认证信息
将用户认证信息存入缓存中,用于后续请求用token访问时能在过滤器中获取到认证信息
在登录业务中根据用户认证信息获取到用户id,根据用户id生成token并封装到响应对象中返回给前端使用
配置JwtAuthenticationTokenFilter
用于每次请求的获取认证信息处理,并且将认证信息存储到存入SecurityContextHolder中去,作为此次请求的线程共享变量
配置类
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//允许跨域
http.cors().and()
//关闭csrf
.csrf().disable()
//不从session获取SpringSecurity
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//允许匿名访问的资源
.antMatchers("/login").anonymous()
// 除上面外的所有请求全部不需要认证即可访问
.anyRequest().permitAll();
http.logout().disable();
//把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中
http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);
}
}
实体类
package com.os467.vo;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
public class UserInfoVo {
/**
* 主键
*/
private Long id;
/**
* 昵称
*/
private String nickName;
/**
* 头像
*/
private String avatar;
private String sex;
private String email;
}
package com.os467.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BlogUserLoginVo {
private String token;
private UserInfoVo userInfo;
}
业务逻辑
BlogLoginService
package com.os467.service.impl;
import com.os467.common.ResponseResult;
import com.os467.entity.LoginUser;
import com.os467.entity.User;
import com.os467.service.BlogLoginService;
import com.os467.utils.BeanCopyUtils;
import com.os467.utils.JwtUtil;
import com.os467.utils.RedisCache;
import com.os467.vo.BlogUserLoginVo;
import com.os467.vo.UserInfoVo;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
@Service
public class BlogLoginServiceImpl implements BlogLoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Override
public ResponseResult login(User user) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
//开始认证
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//获取到LoginUser信息
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
//从LoginUser中获取用户信息
Long userid = loginUser.getUser().getId();
//生成token
String token = JwtUtil.createJWT(userid.toString());
//将用户信息存入redis
redisCache.setCacheObject("bloglogin:"+userid,loginUser);
UserInfoVo userInfoVo = BeanCopyUtils.copyBean(loginUser.getUser(), UserInfoVo.class);
//将用户Vo数据封装进登录用户Vo中
BlogUserLoginVo loginVo = new BlogUserLoginVo(token, userInfoVo);
return ResponseResult.okResult(loginVo);
}
}
UserDetailsService
package com.os467.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.os467.entity.LoginUser;
import com.os467.entity.User;
import com.os467.mapper.UserMapper;
import com.sun.org.apache.xerces.internal.xs.StringList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.Objects;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@SuppressWarnings("all")
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
//根据用户名获取到用户信息
queryWrapper.eq(User::getUserName,username);
User user = userMapper.selectOne(queryWrapper);
if (Objects.isNull(user)){
throw new RuntimeException("用户不存在");
}
//封装loginUser信息
LoginUser loginUser = new LoginUser(user, Arrays.asList("123","22"));
return loginUser;
}
}
*5.1异常处理(必须)
目前我们的项目在认证出错或者权限不足的时候响应回来的Json是Security的异常处理结果,但是这个响应的格式肯定是不符合我们项目的接口规范的,所以需要自定义异常处理
AuthenticationEntryPoint 认证失败处理器
AccessDeniedHandler 授权失败处理器
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
authException.printStackTrace();
//InsufficientAuthenticationException
//BadCredentialsException
ResponseResult result = null;
if(authException instanceof BadCredentialsException){
result = ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_ERROR.getCode(),authException.getMessage());
}else if(authException instanceof InsufficientAuthenticationException){
result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
}else{
result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR.getCode(),"认证或授权失败");
}
//响应给前端
WebUtils.renderString(response, JSON.toJSONString(result));
}
}
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
accessDeniedException.printStackTrace();
ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NO_OPERATOR_AUTH);
//响应给前端
WebUtils.renderString(response, JSON.toJSONString(result));
}
}
配置类配置
//配置异常处理器
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
5.2统一异常处理
实际我们在开发过程中可能需要做很多的判断校验,如果出现了非法情况我们是期望响应对应的提示的,但是如果我们每次都自己手动去处理就会非常麻烦,我们可以选择直接抛出异常的方式,然后对异常进行统一处理,把异常中的信息封装成ResponseResult响应给前端
异常类
package com.os467.exception;
import com.os467.enums.AppHttpCodeEnum;
public class SystemException extends RuntimeException{
private int code;
private String msg;
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
public SystemException(AppHttpCodeEnum httpCodeEnum) {
super(httpCodeEnum.getMsg());
this.code = httpCodeEnum.getCode();
this.msg = httpCodeEnum.getMsg();
}
}
表现层
package com.os467.controller;
import com.os467.common.ResponseResult;
import com.os467.enums.AppHttpCodeEnum;
import com.os467.exception.SystemException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
public class GlobalExceptionHandler {
@ExceptionHandler(SystemException.class)
public ResponseResult systemExceptionHandler(SystemException e){
log.error("出现了异常{}",e);
return ResponseResult.errorResult(e.getCode(),e.getMsg());
}
@ExceptionHandler(Exception.class)
public ResponseResult exceptionHandler(SystemException e){
log.error("出现了异常{}",e);
return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR,e.getMsg());
}
}
5.3登出接口
这里出现了一个异常,在登出接口时出现了fastjosn序列化的报错
报错原因来自从redis反序列化工具中获取LoginUser实例时没有给LoginUser提供默认有参构造,分析可能是在底层先创建出实例再给属性注入数据
解决方案:给LoginUser类加@NoArgsConstructor注解提供无参构造
表现层
@RestController
public class BlogLogoutController {
@Autowired
private BlogLogoutService blogLogoutService;
@PostMapping("/logout")
public ResponseResult logout(){
return blogLogoutService.logout();
}
}
业务层
@Service
public class BlogLogoutServiceImpl implements BlogLogoutService {
@Autowired
private RedisCache redisCache;
/**
* 从SecurityContext中获取到用户信息,删除redis缓存中的用户信息
* @return
*/
@Override
public ResponseResult logout() {
//从SecurityContext中获取到用户认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//获取用户认证信息
LoginUser loginUser = (LoginUser)authentication.getPrincipal();
//从LoginUser中获取userid
Long userid = loginUser.getUser().getId();
//根据userid删除redis缓存中的用户数据
redisCache.deleteObject("bloglogin:" + userid);
return ResponseResult.okResult("登出成功");
}
}
6.评论功能
评论功能,有两种评论为根评论和子评论
通过根评论id可以查询子评论
所有评论都需要设置其用户昵称信息
如果是根评论还要查询其子评论信息
如果是子评论需要查询其回复评论信息(根评论信息)
实体类
package com.os467.entity;
import java.util.Date;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 评论表(Comment)表实体类
*
* @author makejava
* @since 2022-09-29 22:18:53
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuppressWarnings("serial")
public class Comment {
private Long id;
//评论类型(0代表文章评论,1代表友链评论)
private String type;
//文章id
private Long articleId;
//根评论id
private Long rootId;
//评论内容
private String content;
//所回复的目标评论的userid
private Long toCommentUserId;
//回复目标评论id
private Long toCommentId;
private Long createBy;
private Date createTime;
private Long updateBy;
private Date updateTime;
//删除标志(0代表未删除,1代表已删除)
private Integer delFlag;
}
package com.os467.vo;
import lombok.Data;
import java.util.Date;
import java.util.List;
@Data
public class CommentVo {
private Long id;
//文章id
private Long articleId;
//根评论id
private Long rootId;
//评论内容
private String content;
//所回复的目标评论的userid
private Long toCommentUserId;
//回复目标评论id
private Long toCommentId;
private Long createBy;
private Date createTime;
private String toCommentUserName;
//用于存放用户的nickName
private String username;
//用于存放词条评论的子评论信息
private List<CommentVo> children;
}
表现层
@RestController
@RequestMapping("/comment")
public class CommentController {
@Autowired
private CommentService commentService;
@GetMapping("/commentList")
public ResponseResult commentList(Long articleId,Integer pageNum,Integer pageSize){
return commentService.commentList(articleId,pageNum,pageSize);
}
}
业务层
@Service("commentService")
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements CommentService {
@SuppressWarnings("all")
@Autowired
private CommentMapper commentMapper;
@SuppressWarnings("all")
@Autowired
private UserMapper userMapper;
/**
* 根据文章id获取文章评论信息
* @param articleId
* @param pageNum
* @param pageSize
* @return
*/
@Override
public ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize) {
//构造查询条件
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
//查询此文章下的评论信息
queryWrapper.eq(Comment::getArticleId,articleId);
//查询根评论
queryWrapper.eq(Comment::getRootId,-1);
Page<Comment> commentPage = new Page<>(pageNum, pageSize);
//分页查询到该文章下的评论信息
page(commentPage,queryWrapper);
//转换成评论Vo数据
List<CommentVo> commentVos = commentVoList(commentPage.getRecords());
//为根评论设置子评论信息
for (CommentVo commentVo : commentVos) {
//根据根评论id获取子评论列表
List<CommentVo> childrenCommentList = getChildrenCommentList(commentVo.getId());
//存放子评论信息
commentVo.setChildren(childrenCommentList);
}
//设置pageVo对象
PageVo pageVo = new PageVo(commentVos, commentPage.getTotal());
return ResponseResult.okResult(pageVo);
}
/**
* 为所有评论数据存入用户昵称信息
* 如果是子评论还需要设置回复用户信息
* @param records
* @return
*/
private List<CommentVo> commentVoList(List<Comment> records) {
//获取评论列表信息
List<CommentVo> commentVos = BeanCopyUtils.copyBeanList(records, CommentVo.class);
for (CommentVo commentVo : commentVos) {
//根据评论中的创建者userid获取对应用户昵称
String nickName = userMapper.selectById(commentVo.getCreateBy()).getNickName();
//设置创建评论的用户昵称信息
commentVo.setUsername(nickName);
//如果不是根评论还需要设置子评论所回复目标用户的名称
if (commentVo.getToCommentUserId() != -1){
//根据toCommentUserId查询用户并获取用户名称
String toCommentUserName = userMapper.selectById(commentVo.getToCommentUserId()).getNickName();
//设置回复用户名称信息
commentVo.setToCommentUserName(toCommentUserName);
}
}
return commentVos;
}
/**
* 根据根评论id获取根评论对应的所有子评论信息
* @param id
* @return
*/
private List<CommentVo> getChildrenCommentList(Long id) {
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
//查询此根评论下的子评论,按照创建时间降序排序
queryWrapper.eq(Comment::getRootId,id);
queryWrapper.orderByDesc(Comment::getCreateTime);
List<Comment> childrenCommentList = commentMapper.selectList(queryWrapper);
//继续获取该评论列表的Vo信息
List<CommentVo> commentVos = commentVoList(childrenCommentList);
return commentVos;
}
}
7.发表评论接口
需求:
用户登录后可以对文章发表评论,也可以对评论发表回复
用户登录后也可以在友链页面进行评论
实体类
package com.os467.entity;
import java.util.Date;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 评论表(Comment)表实体类
*
* @author makejava
* @since 2022-09-29 22:18:53
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuppressWarnings("serial")
public class Comment {
private Long id;
//评论类型(0代表文章评论,1代表友链评论)
private String type;
//文章id
private Long articleId;
//根评论id
private Long rootId;
//评论内容
private String content;
//所回复的目标评论的userid
private Long toCommentUserId;
//回复目标评论id
private Long toCommentId;
private Long createBy;
private Date createTime;
private Long updateBy;
private Date updateTime;
//删除标志(0代表未删除,1代表已删除)
private Integer delFlag;
}
package com.os467.vo;
import lombok.Data;
import java.util.Date;
import java.util.List;
@Data
public class CommentVo {
private Long id;
//文章id
private Long articleId;
//根评论id
private Long rootId;
//评论内容
private String content;
//所回复的目标评论的userid
private Long toCommentUserId;
//回复目标评论id
private Long toCommentId;
private Long createBy;
private Date createTime;
private String toCommentUserName;
//用于存放用户的nickName
private String username;
//用于存放词条评论的子评论信息
private List<CommentVo> children;
}
表现层
@PostMapping("/comment")
public ResponseResult comment(@RequestBody Comment comment){
return commentService.comment(comment);
}
业务层
/**
* 为文章发表评论,或者回复某条评论
* @param comment
* @return
*/
@Override
public ResponseResult comment(Comment comment) {
//判断是否带有评论信息
if (!StringUtils.hasText(comment.getContent())){
throw new SystemException(AppHttpCodeEnum.CONTEXT_NOT_NULL);
}
//保存评论信息到数据库
save(comment);
return ResponseResult.okResult();
}
*MybatisPlus自动填充功能
为实体类需要被自动填充的属性加上注解,指定在什么模式下填充字段
@TableField(fill = FieldFill.INSERT)
private Long createBy;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
创建元数据处理器
==需要加上@Component注解交由Spring容器管理==
setFieldValByName
参数:
需要提供
package com.os467.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.os467.utils.SecurityUtils;
import org.apache.ibatis.reflection.MetaObject;
import java.util.Date;
@Component
public class BlogMetaObjectHandler implements MetaObjectHandler {
/**
* 插入时需要执行的代码
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
Long userId = SecurityUtils.getUserId();
this.setFieldValByName("createBy",userId,metaObject);
this.setFieldValByName("createTime",new Date(),metaObject);
this.setFieldValByName("updateBy",userId,metaObject);
this.setFieldValByName("updateTime",new Date(),metaObject);
}
/**
* 更新时需要执行的代码
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateBy",SecurityUtils.getUserId(),metaObject);
this.setFieldValByName("updateTime",new Date(),metaObject);
}
}
8.个人信息查询
实体类
package com.os467.vo;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
public class UserInfoVo {
/**
* 主键
*/
private Long id;
/**
* 昵称
*/
private String nickName;
/**
* 头像
*/
private String avatar;
private String sex;
private String email;
}
表现层
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/userInfo")
public ResponseResult userInfo(){
return userService.userInfo();
}
}
业务层
/**
* 用户表(User)表服务实现类
*
* @author makejava
* @since 2022-09-30 17:35:15
*/
@Service("userService")
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@SuppressWarnings("all")
@Autowired
private UserMapper userMapper;
/**
* 查询到用户信息
* @return
*/
@Override
public ResponseResult userInfo() {
//从SecurityContext中获取用户id
Long userId = SecurityUtils.getUserId();
//根据用户id查询到该用户的信息
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getId,userId);
User user = userMapper.selectOne(queryWrapper);
//封装成Vo数据
UserInfoVo userInfoVo = BeanCopyUtils.copyBean(user, UserInfoVo.class);
ResponseResult result = ResponseResult.okResult(userInfoVo);
return result;
}
}
9.头像上传接口
需求
在个人中心点击编辑的时候可以上传头像图片,上传完头像后,可以用于更新个人信息接口
*OOS
因为如果把图片视频等文件上传到自己的应用的Web服务器,在读取图片的时候会占用比较多的资源,影响应用服务器的性能
所以我们一般使用OSS(Object Storage Service对象存储服务)存储图片或视频
引入maven坐标
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>[7.7.0, 7.7.99]</version>
</dependency>
配置文件
spring:
servlet:
multipart:
max-file-size: 2MB #文件最大大小限制
max-request-size: 5MB #请求大小限制
oss:
accessKey: xxxx
secretKey: xxxx
bucket: os467-blog-img #对应七牛云的命名空间
表现层
@RestController
public class UploadController {
@Autowired
private UploadService uploadService;
@RequestMapping("/upload")
public ResponseResult uploadImg(MultipartFile img){
return uploadService.uploadImg(img);
}
}
业务层
使用ConfigurationProperties注解来为类的属性注入配置的属性值
必须提供set方法
@Service
@Data
@ConfigurationProperties(prefix = "oss")
public class UploadServiceImpl implements UploadService {
private String accessKey;
private String secretKey;
private String bucket;
private String domainName;
/**
* 上传图片
* @param img
* @return
*/
@Override
public ResponseResult uploadImg(MultipartFile img) {
//判断原始文件名
String originalFilename = img.getOriginalFilename();
if (!originalFilename.endsWith(".png")){
throw new SystemException(AppHttpCodeEnum.FILE_TYPE_ERROR);
}
//通过文件校验则上传到oss
String filePath = PathUtils.generateFilePath(originalFilename);
String url = uploadOSS(img,filePath);
return ResponseResult.okResult(url);
}
/**
* 上传到oss
* @param img
* @param filePath
* @return
*/
private String uploadOSS(MultipartFile img, String filePath) {
//构造一个带指定Region对象的配置类
Configuration cfg = new Configuration(Region.autoRegion());
UploadManager uploadManager = new UploadManager(cfg);
//...生成上传凭证,然后准备上传
String key = filePath;
try {
//获得输入流对象
InputStream inputStream = img.getInputStream();
//创建上传凭证信息
Auth auth = Auth.create(accessKey, secretKey);
String uploadToken = auth.uploadToken(bucket);
Response response = uploadManager.put(inputStream, key, uploadToken, null, null);
//解析上传成功的结果
DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
System.out.println(putRet.key);
System.out.println(putRet.hash);
} catch (QiniuException ex) {
Response r = ex.response;
System.err.println(r.toString());
try {
System.err.println(r.bodyString());
} catch (QiniuException ex2) {
//ignore
}
} catch (IOException e) {
e.printStackTrace();
}
//返回图片所在网址
return domainName+"/"+key;
}
}
10.注册功能
需求
要求用户能够在注册界面完成用户的注册,要求用户名,昵称,邮箱不能和数据库中原有的数据重复
如果某项重复了注册失败并且要有对应的提示,并且要求用户名,密码,昵称,邮箱都不能为空
密码明文加密为秘文后存入数据库
表现层
@PostMapping("/register")
public ResponseResult register(@RequestBody User user){
return userService.register(user);
}
业务层
/**
* 注册用户信息
* @param user
* @return
*/
@Override
public ResponseResult register(User user) {
//对数据进行非空判断
if (!StringUtils.hasText(user.getUserName())){
throw new SystemException(AppHttpCodeEnum.USERNAME_NOT_NULL);
}
if (!StringUtils.hasText(user.getEmail())){
throw new SystemException(AppHttpCodeEnum.EMAIL_NOT_NULL);
}
if (!StringUtils.hasText(user.getPassword())){
throw new SystemException(AppHttpCodeEnum.PASSWORD_NOT_NULL);
}
if (!StringUtils.hasText(user.getNickName())){
throw new SystemException(AppHttpCodeEnum.NICKNAME_NOT_NULL);
}
//对数据是否存在进行判断
if (userNameExist(user.getUserName())){
throw new SystemException(AppHttpCodeEnum.USERNAME_EXIST);
}
if (nickNameExist(user.getNickName())){
throw new SystemException(AppHttpCodeEnum.NICKNAME_EXIST);
}
//使用BCrypt加密原始密码
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encodePassword = passwordEncoder.encode(user.getPassword());
//将加密好的密码存入数据库
user.setPassword(encodePassword);
//存入数据库
save(user);
return ResponseResult.okResult();
}
/**
* 查询判断用户昵称是否存在
* @param nickName
* @return
*/
private boolean nickNameExist(String nickName) {
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getNickName, nickName));
if (Objects.nonNull(user)){
return true;
}
return false;
}
/**
* 查询判断用户名是否存在
* @param userName
* @return
*/
private boolean userNameExist(String userName) {
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUserName, userName));
if (Objects.nonNull(user)){
return true;
}
return false;
}
11.AOP实现日志收集
需要通过日志记录接口调用信息,便于后期调试排查,并且可能有很多接口都需要进行日志的记录
日志打印格式
log.info("=======Start=======");
// 打印请求 URL
log.info("URL : {}",);
// 打印描述信息
log.info("BusinessName : {}", );
// 打印 Http method
log.info("HTTP Method : {}", );
// 打印调用 controller 的全路径以及执行方法
log.info("Class Method : {}.{}", );
// 打印请求的 IP
log.info("IP : {}",);
// 打印请求入参
log.info("Request Args : {}",);
// 打印出参
log.info("Response : {}", );
// 结束后换行
log.info("=======End=======" + System.lineSeparator());
实现aop通常我们可以通过切入点表达式或者自定义注解的方式来配置切面
自定义注解
@Pointcut("@annotation(com.os467.annotation.SystemLog)")
public void pt(){
}
配置切面
package com.os467.ascept;
import com.alibaba.fastjson.JSON;
import com.os467.annotation.SystemLog;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.FieldSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;
import sun.reflect.generics.tree.ClassSignature;
import javax.servlet.http.HttpServletRequest;
@Component
@Aspect
@Slf4j
public class LogAspect {
@Pointcut("@annotation(com.os467.annotation.SystemLog)")
public void pt(){
}
/**
* 配置切面
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("pt()")
public Object printLog(ProceedingJoinPoint joinPoint) throws Throwable {
//获取目标方法的返回值
Object ret;
try {
//前置通知
printBefore(joinPoint);
//执行目标方法
ret = joinPoint.proceed();
//后置通知
printAfter(ret);
} finally {
// 结束后换行,获取到当前系统的换行符
log.info("=======End=======" + System.lineSeparator());
}
//返回切入点方法执行结果
return ret;
}
/**
* 执行方法后需要打印的通知
* @param ret
*/
private void printAfter(Object ret) {
// 打印出参
log.info("Response : {}",JSON.toJSONString(ret));
}
/**
* 执行方法之前需要打印的通知
*/
private void printBefore(ProceedingJoinPoint joinPoint) {
//获取到当前线程的请求对象
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request = null;
//向下转型
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)attributes;
//获取到当前线程请求对象
request = requestAttributes.getRequest();
//通过反射获取被增强方法上的注解对象
SystemLog systemLog = getSystemLog(joinPoint);
log.info("=======Start=======");
// 打印请求 URL
log.info("URL : {}",request.getRequestURL());
// 打印描述信息
log.info("BusinessName : {}",systemLog.businessName());
// 打印 Http method
log.info("HTTP Method : {}", request.getMethod());
// 打印调用 controller 的全路径以及执行方法
log.info("Class Method : {}.{}", joinPoint.getSignature().getDeclaringType(),((MethodSignature)joinPoint.getSignature()).getName());
// 打印请求的 IP
log.info("IP : {}",request.getRemoteHost());
// 打印请求入参
log.info("Request Args : {}", JSON.toJSONString(joinPoint.getArgs()));
}
/**
* 获取到被增强方法上的注解信息
* @param joinPoint
* @return
*/
private SystemLog getSystemLog(ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
SystemLog systemLog = methodSignature.getMethod().getAnnotation(SystemLog.class);
return systemLog;
}
}
表现层
@SystemLog(businessName = "用户信息修改")
@PutMapping("/userInfo")
public ResponseResult updateUser(@RequestBody User user){
return userService.updateUser(user);
}
12.更新浏览次数
Question:在用户浏览博文时需要对博客浏览量进行新增,但是如果在高并发场景下,每次浏览对数据库数据的更新会产生写锁,会对其它用户的访问造成影响
因此我们选择用redis来缓存我们的浏览量数据
为了防止redis关闭造成的数据流失,我们需要定时将redis中的数据同步到mysql数据库中
需求
①在应用启动时把博客的浏览量存储到redis中
②更新浏览量时去更新redis中的数据
③每隔10分钟把Redis中的浏览量更新到数据库中
④读取文章浏览量时从redis读取
将浏览量存储到redis
实现CommandLineRunner接口,在应用启动时初始化缓存
@Component
public class BlogCommendLineRunner implements CommandLineRunner {
@SuppressWarnings("all")
@Autowired
private ArticleMapper articleMapper;
@Autowired
private RedisCache redisCache;
/**
* springboot项目启动后执行的方法
* @param args
* @throws Exception
*/
@Override
public void run(String... args) throws Exception {
//获取浏览数据列表
List<Article> articles = articleMapper.selectList(null);
//存储为map集合
Map<String, Integer> map = articles.stream().collect(Collectors
.toMap(article -> article.getId().toString(),
article -> article.getViewCount().intValue()));
//存入redis缓存
redisCache.setCacheMap("article:viewCount",map);
}
}
redis缓存工具类添加方法
increment()
为某个指定key的long或double型数据加减多少数值
/**
* 更新缓存中的集合数据
* @param key
* @param hKey
* @param value
*/
public void incrementCacheMap(String key,String hKey,Long value){
redisTemplate.boundHashOps(key).increment(hKey,value);
}
表现层
@PutMapping("/updateViewCount/{id}")
public ResponseResult updateViewCount(@PathVariable("id") Long id){
return articleService.updateViewCount(id);
}
业务层
/**
* 上传更新数据
* @param id
* @return
*/
@Override
public ResponseResult updateViewCount(Long id) {
//更新缓存内数据
redisCache.incrementCacheMap("article:viewCount",id.toString(),1L);
return ResponseResult.okResult();
}
修改文章查询时浏览量数据
//根据缓存中的数据更新浏览量
article.setViewCount(redisCache.getCacheMapValue("article:viewCount",article.getId().toString()));
==这里其实有问题,因为redis在存入long类型数据的时候会转化为int类型,所以取出数据的时候需要进行强转==
修改方案
article.setViewCount(getViewCount(article.getId().toString()));
/**
* 根据文章id从redis获取浏览量
* @param articleId
* @return
*/
private Long getViewCount(String articleId) {
Object viewCount = redisCache.getCacheMapValue("article:viewCount", articleId);
return Long.parseLong(viewCount.toString());
}
*定时任务
定时任务的实现方式有很多,比如XXL-job,但是其核心功能和概念都是类似的,很多情况下只是调用的API不同而已
使用Springboot提供的定时任务API来实现一个简单的定时任务
在配置类上加上注释 @EnableScheduling 开启定时任务功能
@Component
public class BlogScheduleJob {
@Autowired
private ArticleService articleService;
@Autowired
private RedisCache redisCache;
@Scheduled(cron = "0 0/10 * * * ?")
public void scheduleJob(){
//从缓存中获取到文章浏览量数据
Map<String, Object> map = redisCache.getCacheMap("article:viewCount");
//获取id集合
Set<String> idSet = map.keySet();
//构建用于存放文章浏览信息的数组
List<Article> articles = new ArrayList<>();
//获取id集合迭代对象
Iterator<String> ids = idSet.iterator();
//遍历id集合
while (ids.hasNext()){
String id = ids.next();
Article article = new Article(Long.parseLong(id),Long.parseLong(map.get(id).toString()));
//向数组中添加文章数据
articles.add(article);
}
//更新数据库中的数据
articleService.updateBatchById(articles);
}
}
这里还有一种更加简洁的写法
//也可以采用下面的方式
List<Article> articles = map.entrySet().stream().map((item) -> {
String key = item.getKey();
Object value = item.getValue();
return new Article(Long.valueOf(key), Long.valueOf(value.toString()));
}).collect(Collectors.toList());
注意从redis中取出的int数据需要强转为long型,集合需要获取其迭代对象来遍历集合
==知识点==: ValueOf方法得到的是Long包装类,parseLong方法得到的是long数据类型
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以邮件至 1300452403@qq.com