1.需求分析
- 注册功能(添加用户操纵)
- 登录功能(查询操作)
- 我的文章列表页(查询我的文章|文章修改|文章详情|文章删除)
- 博客编辑页(添加文章操作)
- 所有人博客列表(带分页功能)
- 文章详情页(多个查询功能和一个修改功能)
2.设计数据库
用户表:id(主键),登录名,密码,头像,状态
文章表:主键,标题,正文,发表时间,更新时间,作者id,阅读量,状态。
3.创建项目所有框架
所用技术:Spring Boot + Spring MVC + MyBatis + MySQL
将前端代码添加到项目中,前端代码详情请看博客连接。
创建后端并进行项目分层:
- 控制层
- 服务层
- 数据持久层
- 实体类层
- 配置层
- 工具层
4.后端代码
4.1实体类层
用户类:
package com.example.demo.model;
import lombok.Data;
import java.time.LocalDateTime;
/**
* Describe:
* User:lenovo
* Date:2023-08-03
* Time:18:02
*/
@Data
public class Userinfo {
private int id;
private String username;
private String password;
private String photo;
private LocalDateTime createtime;
private LocalDateTime updatetime;
private int state;
}
文章类:
package com.example.demo.model;
import lombok.Data;
import java.time.LocalDateTime;
/**
* Describe:
* User:lenovo
* Date:2023-08-03
* Time:18:04
*/
@Data
public class Articleinfo {
private int id;
private String title;
private String content;
private LocalDateTime createtime;
private LocalDateTime updatetime;
private int uid;
private int rcount;
private int state;
}
4.2注册功能
在common层编写ResultAjax类,为统一的返回类。
@Data
public class ResultAjax {
private int code;
private String msg;
private Object data;
//为了方便使用我们构造一个成功方法
public static ResultAjax succ(Object data) {
ResultAjax resultAjax = new ResultAjax();
resultAjax.setCode(200);
resultAjax.setData(data);
return resultAjax;
}
//为了方便处理构造一个成功的方法
public static ResultAjax succ(int code, String msg, Object data) {
ResultAjax resultAjax = new ResultAjax();
resultAjax.setData(code);
resultAjax.setMsg(msg);
resultAjax.setData(data);
return resultAjax;
}
//为了方便,我们构造一个失败的方法
public static ResultAjax fail(int code, String msg) {
ResultAjax resultAjax = new ResultAjax();
resultAjax.setCode(code);
resultAjax.setMsg(msg);
resultAjax.setData(null);
return resultAjax;
}
//为了方便,我们构造一个失败的方法
public static ResultAjax fail(int code, String msg, Object data) {
ResultAjax resultAjax = new ResultAjax();
resultAjax.setCode(code);
resultAjax.setMsg(msg);
resultAjax.setData(data);
return resultAjax;
}
}
编写UserMapper类:
@Mapper
public interface UserMapper {
@Insert("insert into userinfo(username, password) values(#{username}, #{password})")
int reg(String username, String password);
}
编写UserService类:
@Service
public class UserService {
@Autowired
UserMapper userMapper;
public int reg(String username, String password) {
return userMapper.reg(username, password);
}
}
编写UserController类:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/reg")
public ResultAjax reg(Userinfo userinfo) {
//1.参数校验
if(userinfo.getUsername() == null || userinfo.getUsername().trim().equals("")) {
return ResultAjax.fail(-1, "参数错误!");
}
if(userinfo.getPassword() == null || userinfo.getPassword().trim().equals("")) {
return ResultAjax.fail(-1, "参数错误");
}
//2.处理数据
int ret = userService.reg(userinfo.getUsername(), userinfo.getPassword());
//3.返回结果
return ResultAjax.succ(ret);
}
}
4.3登录功能
VO实体对象类是实体对象类的一个扩展,主要包含数据表中不拥有,但是前后端可能会使用到的对象。比如登录的时候,我们需要验证码,这个验证码,必须后端接收,并且不存在于数据表中,我们可以使用扩展对象进行接收。
编写UserVO类(在此文章中作用不大,但是方便以后的更新):
/**
* Describe: userinfo的扩展类
* User:lenovo
* Date:2023-08-04
* Time:19:03
*/
@Data
public class UserinfoVO extends Userinfo {
//这里可以放入验证码
private String checkCode;
}
编写UserMapper类:
@Select("select * from userinfo where username=#{username}")
Userinfo getUserByName(@Param("username")String username);
编写UserService类:
public Userinfo getUserByName(String username) {
return userMapper.getUserByName(username);
}
编写UserController类:
//登录接口:根据名字查找用户
@RequestMapping("/login")
public ResultAjax getUserByName(UserinfoVO userinfoVO, HttpServletRequest request) {
// 1.校验参数
if(userinfoVO == null || !StringUtils.hasLength(userinfoVO.getUsername()) ||
!StringUtils.hasLength(userinfoVO.getPassword())) { //这里我们使用StringUtils.hasLength判断它是否为null或为空
return ResultAjax.fail(-1, "非法参数!");
}
// 2.根据用户名查找对象
Userinfo userinfo = userService.getUserByName(userinfoVO.getUsername());
if(!userinfoVO.getPassword().equals(userinfo.getPassword())) {
//密码错误
return ResultAjax.fail(-2, "账号或密码错误!");
}
// 3.将对象存储到session中
HttpSession session = request.getSession();
session.setAttribute(AppVariable.SESSION_USERINFO_KEY, userinfo);
// 4.将结果返回给前端
return ResultAjax.succ(1);
}
这里我们使用cookie-session才储存用户信息。
编写公共类(通过这个类,我们知道session通过什么关键字,来获取到用户的关键信息):
/**
* Describe: 全局变量
* User:lenovo
* Date:2023-08-04
* Time:19:12
*/
public class AppVariable {
//用户的session key
public static final String SESSION_USERINFO_KEY = "SESSION_USERINFO";
}
4.4登录校验/统一异常处理/统一结果返回
登录校验:
/**
* Describe:拦截器
* User:lenovo
* Date:2023-08-09
* Time:19:06
*/
public class LoginIntercept implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
HttpSession session = request.getSession(false);
if(session == null || session.getAttribute(AppVariable.SESSION_USERINFO_KEY) == null) {
response.sendRedirect("/login.html");
return false;
}
return true;
}
}
/**
* Describe:系统配置文件
* User:lenovo
* Date:2023-08-09
* Time:19:15
*/
@Configuration
public class MyConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginIntercept())
.addPathPatterns("/**")
.excludePathPatterns("/user/login")
.excludePathPatterns("/user/reg")
.excludePathPatterns("/art/getlistbypage")
.excludePathPatterns("/art/detail")
.excludePathPatterns("/editor.md/*")
.excludePathPatterns("/img/*")
.excludePathPatterns("/js/*")
.excludePathPatterns("/css/*")
.excludePathPatterns("/blog_list.html")
.excludePathPatterns("/blog_content.html")
.excludePathPatterns("/reg.html")
.excludePathPatterns("/login.html");
}
}
统一结果返回:
/**
* Describe:保底统一的返回值
* User:lenovo
* Date:2023-08-05
* Time:10:00
*/
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//判断类是否为标准的返回类型
if(body instanceof ResultAjax) {
return body;
}
if(body instanceof String) {
ResultAjax resultAjax = ResultAjax.succ(body);
try {
return objectMapper.writeValueAsString(resultAjax);//如果是String类型,我们需要手动的转化为json对象
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return ResultAjax.succ(body);
}
}
统一异常的处理:
/**
* Describe:统一异常的处理
* User:lenovo
* Date:2023-08-05
* Time:10:33
*/
//@RestControllerAdvice //为了发现后续编程过程中的问题,我们可以先注释掉
public class ExceptionAdvice {
@ExceptionHandler(Exception.class)
public ResultAjax doException(Exception e) {
return ResultAjax.fail(-1, e.getMessage());
}
}
4.5查看我的文章列表页
Mapper:
@Mapper
public interface ArticleMapper {
@Select("select * from articleinfo where uid = #{uid} order by id desc")
List<Articleinfo> getListByUid(@Param("uid") int uid);
}
service:
@Service
public class ArticleService {
@Autowired
private ArticleMapper articleMapper;
public List<Articleinfo> getListByUid(@Param("uid") int uid) {
return articleMapper.getListByUid(uid);
}
}
controller:
//得到当前登录用户的文章列表
@RequestMapping("/mylist")
public ResultAjax myList(HttpServletRequest request) {
// 1.等到当前登录的用户
Userinfo userinfo = SessionUtils.getUser(request);
if(userinfo == null) {
return ResultAjax.fail(-1, "请先登录!");
}
// 2.根据用户的id查询你次用户发表的所有文章
List<Articleinfo> list = articleService.getListByUid(userinfo.getId());
//处理list->将文章正文变成简介
if(list != null && list.size() > 0) {
//并发的处理文章的内容,然后处理集合中每一个元素
list.stream().parallel().forEach((art) -> {
if(art.getContent().length() > _DESC_LENGTH) {
//这是简介内容,我们取前120个字符
art.setContent(art.getContent().substring(0, _DESC_LENGTH));
}
});
}
return ResultAjax.succ(list);
}
4.6添加博客
Mapper:
@Select("select * from userinfo where username=#{username}")
Userinfo getUserByName(@Param("username")String username);
service:
public int add(Articleinfo articleinfo) {
return articleMapper.add(articleinfo);
}
controller:
//添加文章
@RequestMapping("/add")
public ResultAjax add(Articleinfo articleinfo, HttpServletRequest request) {
// 1.校验参数
if(articleinfo == null || !StringUtils.hasLength(articleinfo.getTitle()) ||
!StringUtils.hasLength(articleinfo.getContent())) {
return ResultAjax.fail(-1, "非法参数!");
}
// 2.组装数据
Userinfo userinfo = SessionUtils.getUser(request);
if(userinfo == null) {
return ResultAjax.fail(-2, "请先登录");
}
articleinfo.setUid(userinfo.getId());
int ret = articleService.add(articleinfo);
// 3.将结果返回给前端
return ResultAjax.succ(ret);
}
4.7删除博客
Mapper:
@Delete("delete from articleinfo where id = #{aid} and uid = uid")
int del(@Param("aid") Integer aid, int uid);
Service:
public int del(Integer aid, int uid) {
return articleMapper.del(aid, uid);
}
Controller:
//删除文章
@RequestMapping("/del")
public ResultAjax del(Integer aid, HttpServletRequest request) {
// 1.参数校验
if(aid == null || aid <= 0) {
return ResultAjax.fail(-1, "参数错误");
}
// 2.获取登录的用户
Userinfo userinfo = SessionUtils.getUser(request);
if(userinfo == null) {
return ResultAjax.fail(-1, "请先登录");
}
// 3.执行删除操作
// 执行删除操作,我们有很多的方式,这里采用 aid和uid 的方法
int ret = articleService.del(aid, userinfo.getId());
if(ret == 0) {
return ResultAjax.fail(-1, "你不是文章的归属人");
}
return ResultAjax.succ(ret);
}
4.8查看文章详情
查看文章主要包含三个功能:
- 查询文章的内容
- 查询相应的作者信息
- 查询文章总数
最后两点我们可以使用多线程的方式来实现,提高查询的效率,同时三个功能使用一个HTTP请求,以减少多次连接和断开的次数,减少资源的消耗.
ArticleMapper:
@Select("select * from articleinfo where id = #{aid}")
Articleinfo getDetailById(@Param("aid") int aid);
@Select("select count(*) from articleinfo where uid = #{uid}")
int getArtCountByUid(@Param("uid") int uid);
UserMapper:
@Select("select * from userinfo where id = #{uid}")
UserinfoVO getUseById(@Param("uid") int uid);
ArticleService:
public Articleinfo getDetail(int aid) {
return articleMapper.getDetailById(aid);
}
public int getArtCountByUid(int uid) {
return articleMapper.getArtCountByUid(uid);
}
UserService:
public UserinfoVO getUserById(int uid) {
return userMapper.getUseById(uid);
}
ArticleController:
//获取文章详情
@RequestMapping("/detail")
public ResultAjax detail(Integer aid) throws ExecutionException, InterruptedException {
// 1.参数校验
if(aid == null || aid <= 0) {
return ResultAjax.fail(-1, "参数错误!");
}
// 2.查看文章详情
Articleinfo articleinfo = articleService.getDetail(aid);
if(articleinfo == null) {
return ResultAjax.fail(-2, "查询文章不存在");
}
// 3.根据uid查询作者的详情信息
FutureTask<UserinfoVO> userTask = new FutureTask<>(() -> {
return userService.getUserById(articleinfo.getUid());
});
// 4.查询uid查询用户发表的文章总数
FutureTask<Integer> artCountTask = new FutureTask<>(() -> {
return articleService.getArtCountByUid(articleinfo.getUid());
});
taskExecutor.submit(artCountTask);
taskExecutor.submit(userTask);
// 5.组装数据
UserinfoVO userinfoVO = userTask.get(); //等待任务执行完成
int artCount = artCountTask.get(); //等待任务执行完成
userinfoVO.setArtCount(artCount);
HashMap<String, Object> result = new HashMap<>();
result.put("user", userinfoVO);
result.put("art", articleinfo);
// 6.返回结果给前端
return ResultAjax.succ(result);
}
增加阅读量:
Mapper:
@Update("update articleinfo set rcount = rcount + 1 where id = #{aid}")
int incrementRCount(@Param("aid") int aid);
Service:
public int incrementRCount(int aid) {
return articleMapper.incrementRCount(aid);
}
Controller:
//文章阅读量加1
@RequestMapping("/increment_rcount")
public ResultAjax incrementRCount(Integer aid) {
// 1.参数校验
if(aid == null || aid <= 0) {
return ResultAjax.fail(-1, "参数错误");
}
// 2.更改数据库
int result = articleService.incrementRCount(aid);
// 3.返回结果
return ResultAjax.succ(result);
}
4.9修改文章
Mapper:
@Select("select * from articleinfo where id = #{aid} and uid = #{uid}")
Articleinfo getArticleByIdAndUid(int aid, int uid);
@Update("update articleinfo set title=#{title}, content=#{content} where id = #{id} and uid = #{uid}")
int update(Articleinfo articleinfo);
service:
public Articleinfo getArticleByIdAndUid(int aid, int uid) {
return articleMapper.getArticleByIdAndUid(aid, uid);
}
public int update(Articleinfo articleinfo) {
return articleMapper.update(articleinfo);
}
controller:
//修改文章
//更新文章
@RequestMapping("/update")
public ResultAjax update(Articleinfo articleinfo, HttpServletRequest request) {
// 1.参数校验
if(articleinfo == null || articleinfo.getId() <= 0 ||
!StringUtils.hasLength(articleinfo.getTitle()) ||
!StringUtils.hasLength(articleinfo.getContent())) {
return ResultAjax.fail(-1, "参数错误!");
}
// 2.获取登录的用户
Userinfo userinfo = SessionUtils.getUser(request);
if(userinfo == null) {
return ResultAjax.fail(-2, "请登录!");
}
// 3.操作数据库
articleinfo.setUid(userinfo.getId());
int ret = articleService.update(articleinfo);
return ResultAjax.succ(ret);
}
//修改文章
//获取文章
@RequestMapping("/update_init")
public ResultAjax updateInit(Integer aid, HttpServletRequest request) {
// 1.参数校验
if(aid == null || aid <= 0) {
return ResultAjax.fail(-1, "参数错误!");
}
// 2.操作数据库
Userinfo userinfo = SessionUtils.getUser(request);
if(userinfo == null) {
return ResultAjax.fail(-2, "请登录");
}
//我们利用where id = #{aid} and uid = #{uid}的操作校验文章所有者
Articleinfo articleinfo = articleService.getArticleByIdAndUid(aid, userinfo.getId());
// 3.返回结果
return ResultAjax.succ(articleinfo);
}
4.10文章列表页
文章列表页,主要包含两部分:
- 文章的获取
- 分页功能
mapper:
@Select("select * from articleinfo order by id desc limit #{psize} offset #{offset}")
List<Articleinfo> getListByPage(@Param("psize") int psize, @Param("offset") int offset);
@Select("select count(*) from articleinfo")
int getCount();
service:
public List<Articleinfo> getListByPage(int psize, int offset) {
return articleMapper.getListByPage(psize, offset);
}
public int getCount() {
return articleMapper.getCount();
}
controller:
//查询分页功能
@RequestMapping("/getlistbypage")
public ResultAjax getListByPage(Integer pindex, Integer psize) throws ExecutionException, InterruptedException {
// 1.参数校验
if(pindex == null) {
pindex = 1;
}
if(psize == null) {
psize = 2;
}
// 2.并发执行文章列表和总页数的查询
int finalOffset = psize * (pindex - 1); // 分页公式
int finalPSize = psize;
FutureTask<List<Articleinfo>> listTask = new FutureTask<>(() -> {
return articleService.getListByPage(finalPSize, finalOffset);
});
FutureTask<Integer> sizeTask = new FutureTask<Integer>(() -> {
int totalCount = articleService.getCount();
if(totalCount % finalPSize != 0) {
return totalCount / finalPSize + 1;
}
return totalCount/ finalPSize;
});
taskExecutor.submit(listTask);
taskExecutor.submit(sizeTask);
// 3.组装数据
List<Articleinfo> list = listTask.get();
int size = sizeTask.get();
HashMap<String, Object> map = new HashMap<>();
map.put("list", list);
map.put("size", size);
// 4.将结果返回给前端
return ResultAjax.succ(map);
}
4.11注销功能
Controller:
//注销功能
@RequestMapping("/logout")
public ResultAjax logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if(session != null && session.getAttribute(AppVariable.SESSION_USERINFO_KEY) != null) {
session.removeAttribute(AppVariable.SESSION_USERINFO_KEY);
}
return ResultAjax.succ(1);
}
5.加盐算法
/**
* Describe: 密码工具类
* User:lenovo
* Date:2023-08-10
* Time:10:45
*/
public class PasswordUtils {
/**
* 加盐加密
*/
public static String encrypt(String password) {
// 1.盐值
String salt = UUID.randomUUID().toString().replace("-", "");
// 2.将盐值+密码进行md5得到最终密码
String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes(StandardCharsets.UTF_8));
// 3.将盐值和最终密码进行返回
return salt + "$" + finalPassword;
}
/**
* 加盐验证
*/
public static boolean decrypt(String password, String dbpassword) {
if(!StringUtils.hasLength(password) || !StringUtils.hasLength(dbpassword) ||
dbpassword.length() != 65) {
return false;
}
// 1.得到盐值
String[] dbPasswordArray = dbpassword.split("\\$");
if(dbPasswordArray.length != 2) {
return false;
}
// 盐值
String salt = dbPasswordArray[0];
// 最终密码
String dbFinalPassword = dbPasswordArray[1];
// 2.加密待验证的密码
String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes(StandardCharsets.UTF_8));
// 3.对比
return finalPassword.equals(dbFinalPassword);
}
}
修改原来的UserController:
//注册功能:添加用户
@RequestMapping("/reg")
public ResultAjax reg(Userinfo userinfo) {
//1.参数校验
if(userinfo.getUsername() == null || userinfo.getUsername().trim().equals("")) {
return ResultAjax.fail(-1, "参数错误!");
}
if(userinfo.getPassword() == null || userinfo.getPassword().trim().equals("")) {
return ResultAjax.fail(-1, "参数错误");
}
//2.处理数据
int ret = userService.reg(userinfo.getUsername(), PasswordUtils.encrypt(userinfo.getPassword()));
//3.返回结果
return ResultAjax.succ(ret);
}
//登录接口:根据名字查找用户
@RequestMapping("/login")
public ResultAjax getUserByName(UserinfoVO userinfoVO, HttpServletRequest request) {
// 1.校验参数
if(userinfoVO == null || !StringUtils.hasLength(userinfoVO.getUsername()) ||
!StringUtils.hasLength(userinfoVO.getPassword())) { //这里我们使用StringUtils.hasLength判断它是否为null或为空
return ResultAjax.fail(-1, "非法参数!");
}
// 2.根据用户名查找对象
Userinfo userinfo = userService.getUserByName(userinfoVO.getUsername());
if(!PasswordUtils.decrypt(userinfoVO.getPassword(), userinfo.getPassword())) {
//密码错误
return ResultAjax.fail(-2, "账号或密码错误!");
}
// 3.将对象存储到session中
HttpSession session = request.getSession();
session.setAttribute(AppVariable.SESSION_USERINFO_KEY, userinfo);
// 4.将结果返回给前端
return ResultAjax.succ(1);
}