# we-blog **Repository Path**: MyGit_Test/we-blog ## Basic Information - **Project Name**: we-blog - **Description**: 基于SpringBoot + Mybatis-Plus的个人博客 - **Primary Language**: Java - **License**: AFL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-02-06 - **Last Updated**: 2023-04-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # WeBlog项目 ## 项目准备 在将项目环境搭建好之后,我们需要明确实体需要有哪些(下面已经画出来了): 各个实体之间的关系为: - 一对一: 博客blog和分类category() - 一对多: 评论comment和回复comment - 多对多: 博客blog和标签tag 需要创建数据库: ![db_entity.png](https://gitee.com/MyGit_Test/we-blog/raw/master/project_imgs/db_entity.png) 对应的建表语句为: ```sql create table if not exists tb_blog ( id int primary key auto_increment, title varchar(256) not null, blog_description mediumtext not null, views bigint default 0, likes bigint default 0, content mediumtext, blog_status int default 0, -- blog是否已经发布 enable_comment int default 1, -- 是否开启了评论 user_id int not null, category_id int, create_time timestamp default current_timestamp, update_time timestamp default current_timestamp ); create table if not exists tb_comment ( id int primary key auto_increment, user_id int not null, blog_id int not null, likes int default 0, comment_body mediumtext, comment_status int default 0, comment_type int, create_time timestamp default current_timestamp, update_time timestamp default current_timestamp ); create table if not exists tb_blog_tag( id int primary key auto_increment, blog_id int not null, tag_id int not null ); create table if not exists tb_tag( id int primary key auto_increment, name varchar(64) ); create table if not exists tb_category( id int primary key auto_increment, name varchar(64), blog_id int, ); create table if not exists tb_user ( id int primary key auto_increment, icon varchar(256) default '/admin/dist/img/user/user_0.png', nickname varchar(256) not null, password varchar(256) not null, salt varchar(12) default '123456' -- 盐值,用于密码的加密和解密 ); create table if not exists tb_comment_reply ( id int primary key auto_increment, comment_id int not null, reply_id int not null ); ``` ## 后台 ### 用户登录/注册 ![login_process.png](https://gitee.com/MyGit_Test/we-blog/raw/master/project_imgs/login_process.png) 值得注意的是,在前台将数据发送给后台的时候,password已经经过了1次MD5加密,但是写入到数据库中的密码还需要再经过1次MD5加密,所以我们再查找用户的时候,还需要对password进行1次MD5加密才可以进行查找用户的操作。 对应的代码为: ```html personal blog | Log in
``` CommonController生成验证码的方法: ```java @Controller public class CommonController { @Autowired private StringRedisTemplate stringRedisTemplate; @GetMapping("/common/kaptcha") public void defaultKaptcha(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception { httpServletResponse.setHeader("Cache-Control", "no-store"); httpServletResponse.setHeader("Pragma", "no-cache"); httpServletResponse.setDateHeader("Expires", 0); httpServletResponse.setContentType("image/png"); ShearCaptcha shearCaptcha= CaptchaUtil.createShearCaptcha(150, 30, 4, 2); String verifyCode = shearCaptcha.getCode(); /* 验证码存入session中,不保存到redis中,因为key不可以保证唯一性 此时在并发的情况下,就会导致验证码覆盖 */ httpServletRequest.getSession().setAttribute(SystemConstants.LOGIN_VERIFYCODE, verifyCode); // 输出图片流 shearCaptcha.write(httpServletResponse.getOutputStream()); } } ``` LoginController执行登录操作的代码为: ```java @PostMapping("/login") @ResponseBody public Result doLogin(LoginVo loginVo, HttpSession session){ String username = loginVo.getUsername(); String password = loginVo.getPassword(); String inputVerify = loginVo.getVerifyCode(); String realVerify = (String)session.getAttribute(SystemConstants.LOGIN_VERIFYCODE); if(!inputVerify.equals(realVerify)){ return Result.fail(ResultBean.LOGIN_VERIFY_ERROR, null); } User user = userService.getUserByUsernameAndPassword(username, password); if(user == null){ //用户不存在,执行注册操作 user = new User(); String dbPass = MD5Utils.formPassToDbPass(password); user.setNickname(username); user.setPassword(dbPass); userService.save(user); } UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); //将用户保存到session,之后就可以从session中取出用户 session.setAttribute("user", userDTO); return Result.success(); } ``` AdminController的代码为: ```java @GetMapping({"/", "/index"}) public ModelAndView index(HttpSession session, ModelAndView modelAndView){ //获取当前登录用户总的blog数目 List blogs = blogService.list(); modelAndView.addObject("blogCount", blogs.size()); //获取当前登录用户总的评论数 int commentCount = 0; for(Blog blog : blogs){ //获取这篇blog的评论数(没有回复的评论 + 回复) commentCount += commentService.count(new LambdaQueryWrapper().eq(Comment::getBlogId, blog.getId())); } modelAndView.addObject("commentCount", commentCount); //获取文章分类总数 modelAndView.addObject("categoryCount", categoryService.count()); //获取标签的总数 modelAndView.addObject("tagCount", tagService.count()); modelAndView.setViewName("/admin/index"); return modelAndView; } ``` 登录成功之后,就会来到后台界面,在后台界面中可以看到如下信息: ![backpage_index.png](https://gitee.com/MyGit_Test/we-blog/raw/master/project_imgs/backpage_index.png) ### 博客管理 博客管理的界面如下所示,主要包括的是新增博客,编辑博客,删除博客等管理。 ![blog_manage.png](https://gitee.com/MyGit_Test/we-blog/raw/master/project_imgs/blog_manage.png) 注意的是,当我们点击了博客管理,或者总文章数的时候,就会来到/admin/blog.html的界面,这时候对于数据的操作主要是在blog.js中,可以通过查看blog.js,从而知道相应操作的url。 这里需要着重提一下的是,在blog.js中的下面代码: ```js $("#jqGrid").jqGrid({ url: '/admin/blog/list', datatype: "json", colModel: [ {label: 'id', name: 'id', index: 'id', width: 10, key: true, hidden: true}, {label: '标题', name: 'title', index: 'title', width: 80}, {label: '浏览', name: 'views', index: 'views', width: 30}, {label: '点赞', name: 'likes', index: 'likes', width: 30}, {label: '分类', name: 'categoryName', index: 'categoryName', width: 30}, {label: '状态', name: 'blogStatus', index: 'blogStatus', width: 100, formatter: blogStatusFormatter}, {label: '创作时间', name: 'createTime', index: 'createTime', width: 80, formatter: dateFormatter}, {label: '编辑时间', name: 'updateTime', index: 'updateTime', width: 80, formatter: dateFormatter} ], height: 700, rowNum: 10, rowList: [10, 20, 50], styleUI: 'Bootstrap', loadtext: '信息读取中...', rownumbers: false, rownumWidth: 20, autowidth: true, multiselect: true, pager: "#jqGridPager", jsonReader: { root: "obj.list", page: "obj.currentPage", total: "obj.pages", records: "obj.total" }, prmNames: { page: "currentPage", rows: "pageSize", order: "order", }, ``` 是用来构造表格的,但是首先需要发送请求`/admin/blog/list`,同时会携带参数`prmNames,参数的名字就是currentPage,pageSize,order`,其实携带的参数远不止这些,可以点击F12看清发送的请求就可以知道的,而最后返回来的结果是一个Result实体数据,在这个实体中,除了有状态码code,状态信息msg,还包括了相应数据obj。这时候我们返回来的相应数据PageResult,具体到时候可以查看这个PageResult类的成员。之后通过jsonRead来读取obj中的数据。 而**当进行新增博客,编辑博客,删除博客的时候,执行完毕之后,就会重新执行上面的代码进行渲染。而对于搜索功能中,则虽然是直接利用上面的代码,但是它多发送了一个参数`keyword`,这样就可以根据标题或者分类来找到keyword对应的blog了**。 下面的操作中也是根据类似的道理操作的。 ### 评论管理 ![comment_manage.png](https://gitee.com/MyGit_Test/we-blog/raw/master/project_imgs/comment_manage.png) ### 标签管理 ![tag_manage.png](https://gitee.com/MyGit_Test/we-blog/raw/master/project_imgs/tag_manage.png) ### 分类管理 ![category_manage.png](https://gitee.com/MyGit_Test/we-blog/raw/master/project_imgs/category_manage.png) 其余的操作暂时还没有实现,后续可能会继续进行完善,例如友情链接,系统配置,但是已经实现了退出,只需要让这个session失效即可。 ## 前台 ### 前台首页 ![reception_home.png](https://gitee.com/MyGit_Test/we-blog/raw/master/project_imgs/reception_home.png) 对应的BlogController中的代码为: ```java @GetMapping({"/page","/"}) public ModelAndView page(@RequestParam(value="currentPage", defaultValue = "1", required = false)Integer currentPage, @RequestParam(value="pageSize", defaultValue = "8", required = false)Integer size, ModelAndView modelAndView){ IPage page = new Page(currentPage, size); //获取所有已经发布了的blog,并且根据发布的时间降序排序 blogService.page(page,new LambdaQueryWrapper() .eq(Blog::getBlogStatus, 1) .orderByDesc(Blog::getCreateTime)); List blogs = page.getRecords(); blogs.forEach(blog ->{ //对于每一篇blog,需要设置作者的名字 findUserByBlog(blog); findCategoryByBlog(blog); }); modelAndView.addObject("blogs", blogs); modelAndView.addObject("currentPage", currentPage); modelAndView.addObject("pages", page.getPages());//页数 //获取所有的标签 modelAndView.addObject("tags", tagService.list()); //获取最新发布的blog modelAndView.addObject("newBlogs",blogService.getNewBlogs()); //获取点击最多的blog modelAndView.addObject("hotBlogs", blogService.getHotBlogs()); modelAndView.setViewName("/blog/amaze/index.html"); return modelAndView; } ``` 当在搜索框中输入`/blog/page`,就会来到blog/amaze/index.html,效果如下所示: ![reception_home.png](https://gitee.com/MyGit_Test/we-blog/raw/master/project_imgs/reception_home.png) ### 根据category查看blog 点击某一个分类,例如`admin in 日常随笔`中的日常随笔,就会发送请求`/blog/category?categoryId=xxx`,然后获取数据,在回到`/blag/amaze/list.html`界面.对应的流程为: ![getBlogByCategory.png](https://gitee.com/MyGit_Test/we-blog/raw/master/project_imgs/getBlogByCategory.png) 对应的代码为: ```java @GetMapping("/category") public ModelAndView getBlogByCategoryId(@RequestParam(value = "categoryId", defaultValue = "1", required = false)Integer categoryId, @RequestParam(value="currentPage", defaultValue = "1", required = false)Integer currentPage, @RequestParam(value="pageSize", defaultValue = "4", required= false)Integer pageSize, ModelAndView modelAndView){ IPage page = new Page<>(currentPage, pageSize); blogService.page(page, new LambdaQueryWrapper().eq(Blog::getCategoryId, categoryId)); List blogs = page.getRecords(); blogs.forEach(blog -> { findUserByBlog(blog); findCategoryByBlog(blog); }); modelAndView.addObject("blogs", blogs); modelAndView.addObject("currentPage", currentPage); modelAndView.addObject("pages", page.getPages()); //获取所有的标签 modelAndView.addObject("tags", tagService.list()); //获取所有的最新发布的blog modelAndView.addObject("newBlogs", blogService.getNewBlogs()); //获取浏览最多的blog modelAndView.addObject("hotBlogs", blogService.getHotBlogs()); modelAndView.setViewName("/blog/amaze/list.html"); //获取请求的url,因为在分页列表中需要通过这个url来进行查询的 String url = "/blog/category?categoryId=" + categoryId; modelAndView.addObject("url", url); return modelAndView; } ``` ### 根据标签查看blog 点击右侧栏中某一个热门标签,就会发送请求`/blog/tag?tagId=xxx`,然后就会执行相应的代码,将数据返回到`/blog/amaze/list.html`进行渲染,对应的流程为: ![getBlogByTag.png](https://gitee.com/MyGit_Test/we-blog/raw/master/project_imgs/getBlogByTag.png) 对应的代码为: ```java @GetMapping("/tag") public ModelAndView getBlogByTagId(@RequestParam(value = "tagId", defaultValue = "1", required = false)Integer tagId, @RequestParam(value="currentPage", defaultValue = "1", required = false)Integer currentPage, @RequestParam(value="pageSize", defaultValue = "4", required= false)Integer pageSize, ModelAndView modelAndView){ List blogs = blogService.getBlogsByTagId(tagId, (currentPage - 1) * pageSize, pageSize); blogs.forEach(blog -> { findCategoryByBlog(blog); findUserByBlog(blog); }); modelAndView.addObject("blogs", blogs); modelAndView.addObject("currentPage", currentPage); //获取这个标签一共有多少页记录 modelAndView.addObject("pages", tagService.getPages(tagId, pageSize)); //获取所有的标签 modelAndView.addObject("tags", tagService.list()); //获取所有的最新发布的blog modelAndView.addObject("newBlogs", blogService.getNewBlogs()); //获取浏览最多的blog modelAndView.addObject("hotBlogs", blogService.getHotBlogs()); modelAndView.setViewName("/blog/amaze/list.html"); //获取请求的url String url = "/blog/tag?tagId=" + tagId; modelAndView.addObject("url", url); return modelAndView; } ``` ### 查看某篇blog详情 ![blog_detail.png](https://gitee.com/MyGit_Test/we-blog/raw/master/project_imgs/blog_detail.png) ```java @GetMapping("/detail") public ModelAndView detail(@RequestParam(name="id", defaultValue = "1", required = false)Integer id, @RequestParam(name = "currentPage", defaultValue = "1", required = false)Integer currentPage, @RequestParam(name = "pageSize", defaultValue = "4", required = false)Integer pageSize, ModelAndView modelAndView){ blogService.update().setSql("views = views + 1").eq("id", id).update(); Blog blog = blogService.getOne(new LambdaQueryWrapper().eq(Blog::getId, id)); findUserByBlog(blog); findCategoryByBlog(blog); //1、获取这个blog的所有标签 List tags = tagService.getTagsByBlogId(id); blog.setTags(tags); modelAndView.addObject("blog", blog); //2、获取这个blog的所有评论以及总的评论数 //2.1 先获取所有已经审核了的一级评论 IPage page = new Page<>(currentPage, pageSize); commentService.page(page, new LambdaQueryWrapper().eq(Comment::getBlogId, id) .eq(Comment::getCommentType, CommentConstants.FIRST_COMMENT_TYPE) .eq(Comment::getCommentStatus, CommentConstants.CHECK_DONE) .orderByDesc(Comment::getCreateTime)); List comments = page.getRecords(); //2.2 获取已经审核了的二级评论(也即一级评论的回复) comments.forEach(comment -> { //2.2.1 获取发布一级评论的用户 findUserByComment(comment); //2.2.2 获取当前这一条评论的回复 List replies = commentService.getRepliesByCommentId(comment.getId()); log.info("replies = " + replies); //设置每一条reply的用户 replies.forEach(reply -> { findUserByComment(reply); }); comment.setReplies(replies); }); System.out.println(comments); //获取评论页数 modelAndView.addObject("commentTotal", page.getTotal()); modelAndView.addObject("pages", page.getPages()); modelAndView.addObject("currentPage", currentPage); modelAndView.addObject("comments", comments); //设置url String url = "/blog/detail?id=" + id; modelAndView.addObject("url", url); modelAndView.setViewName("/blog/amaze/detail.html"); return modelAndView; } ``` 但是对于高并发的情况下,我们可能导致浏览数量是不对的,因此我们需要首先将每个blog的浏览数量保存到redis中,之后定义一个定时任务,每隔5分钟将redis中的浏览数量写入到数据库中。所以修改之后的代码为: ```java /* * 获取某一个id的blog的详细内容,同时需要获取这个blog的所有评论 * 对于博客的浏览数量,则是先将其保存到redis中,然后利用redis的incr命令 * 来更新数量。之后定义一个定时任务,每隔多久就将写入到数据库中 */ @GetMapping("/detail") public ModelAndView detail(@RequestParam(name="id", defaultValue = "1", required = false)Integer id, @RequestParam(name = "currentPage", defaultValue = "1", required = false)Integer currentPage, @RequestParam(name = "pageSize", defaultValue = "4", required = false)Integer pageSize, ModelAndView modelAndView){ //1、更新redis中这篇blog的浏览数量,并将更新后的数量返回 long views = stringRedisTemplate.opsForValue().increment(RedisConstants.BLOG_VIEW_KEY + id); Blog blog = blogService.getById(id); blog.setViews((int)views); findUserByBlog(blog); findCategoryByBlog(blog); //2、获取这个blog的所有标签 List tags = tagService.getTagsByBlogId(id); blog.setTags(tags); modelAndView.addObject("blog", blog); //3、获取这个blog的所有评论以及总的评论数 //3.1 先获取所有已经审核了的一级评论 IPage page = new Page<>(currentPage, pageSize); commentService.page(page, new LambdaQueryWrapper().eq(Comment::getBlogId, id) .eq(Comment::getCommentType, CommentConstants.FIRST_COMMENT_TYPE) .eq(Comment::getCommentStatus, CommentConstants.CHECK_DONE) .orderByDesc(Comment::getCreateTime)); List comments = page.getRecords(); int commentTotal = comments.size(); //3.2 获取已经审核了的二级评论(也即一级评论的回复) for(Comment comment : comments) { //3.2.1 获取发布一级评论的用户 findUserByComment(comment); //3.2.2 获取当前这一条评论的回复 List replies = commentService.getRepliesByCommentId(comment.getId()); commentTotal += replies.size(); //设置每一条reply的用户 replies.forEach(reply -> { findUserByComment(reply); }); comment.setReplies(replies); } //获取评论页数 modelAndView.addObject("commentTotal", commentTotal); modelAndView.addObject("pages", page.getPages()); modelAndView.addObject("currentPage", currentPage); modelAndView.addObject("comments", comments); //设置url String url = "/blog/detail?id=" + id; modelAndView.addObject("url", url); modelAndView.setViewName("/blog/amaze/detail.html"); return modelAndView; } ``` 而需要在SpringBoot中定义定时任务,首先需要导入对应的依赖 ```xml org.awaitility awaitility 3.1.2 test ``` 然后在启动类中添加注解`@EnableScheduling`来表示这个项目中存在定时任务,然后在定义一个类,用来存放对应的定时任务的,这里定义的类是BlogViewUpdateTask,因为需要使用定时任务,需要将这个类添加到Bean容器中。然后在这个类的方法上面使用注解`@Scheduled(fixedRate="xxx")`来定时执行这个方法,而fixedRate则是指定执行的间隔。 ```java @Component public class BlogViewUpdateTask { @Resource private StringRedisTemplate stringRedisTemplate; @Resource private IBlogService blogService; //每隔5分钟就将redis中的浏览数量写入到数据库中 @Scheduled(fixedRate = 300000) private void updateBlogView(){ //获取每个blog的key(符合对应格式的key) Set keys = stringRedisTemplate.keys(RedisConstants.BLOG_VIEW_KEY + "*"); for(String key : keys){ //获取对应的blogId int index = key.lastIndexOf(":"); long blogId = Long.parseLong(key.substring(index + 1)); //对于每个key,需要知道他们对应的浏览数量 blogService.update() .eq("id", blogId) .set("views", stringRedisTemplate.opsForValue().get(key)) .update(); } } } ``` 因为我们将浏览数量保存到redis中,之后定期更新到数据库中,因此后台中的博客管理同样需要从redis中获取对应的blog的浏览数量。 ```java @GetMapping("/list") @ResponseBody public Result index(@RequestParam(value = "currentPage", required = false)Long currentPage, @RequestParam(value = "pageSize",required = false)Long pageSize, @RequestParam(value = "keyword",required = false)String keyword, HttpSession session){ UserDTO userDTO = (UserDTO)session.getAttribute(SystemConstants.LOGIN_USER); Integer userId = userDTO.getId(); //获取当前登录用户的所有title符合keyword形式的blog IPage page = new Page<>(currentPage, pageSize); if(keyword == null || keyword.length() <= 0){ //如果keyword为空,那么查询第currentPage的blog,每页有pageSize条记录 blogService.page(page, new LambdaQueryWrapper().eq(Blog::getUserId, userId) .orderByDesc(Blog::getCreateTime)); }else{ blogService.page(page, new LambdaQueryWrapper().eq(Blog::getUserId, userId) .like(Blog::getTitle, keyword) .orderByDesc(Blog::getCreateTime)); } List blogs = page.getRecords(); //获取blog的所在分类 blogs.forEach(blog -> { //从redis中获取blog的浏览数量,如果这个key不存在redis中,那么返回的是null Integer views = Integer.parseInt(stringRedisTemplate.opsForValue().get(RedisConstants.BLOG_VIEW_KEY + blog.getId())); if(views == null){ blog.setViews(0); }else{ blog.setViews(views); } Integer categoryId = blog.getCategoryId(); if(categoryId != null) { blog.setCategoryName(categoryService.getById(categoryId).getName()); } }); List blogDTOs = new ArrayList<>(); blogs.forEach(blog -> { blogDTOs.add(BeanUtil.copyProperties(blog, BlogDTO.class)); }); PageResult pageResult = new PageResult<>(); pageResult.setList(blogDTOs); pageResult.setTotal(page.getTotal()); pageResult.setCurrentPage(currentPage); pageResult.setPages(page.getPages()); return Result.success(pageResult); } ``` ### 发布评论 ```java @PostMapping("/comment") @Transactional @ResponseBody public Result comment(@RequestParam("id")Integer blogId, @RequestParam("verifyCode")String verifyCode, @RequestParam("commentator")String commentator, @RequestParam("commentBody")String commentBody, HttpSession session){ //获取验证码,判断验证码是否一致 String realVerifyCode = (String)session.getAttribute(SystemConstants.LOGIN_VERIFYCODE); if(!verifyCode.equals(realVerifyCode)){ return Result.fail(ResultBean.LOGIN_VERIFY_ERROR, null); } //获取当前评论的用户id User user = userService.getOne(new LambdaQueryWrapper().eq(User::getNickname, commentator)); if(user == null){ return Result.fail(ResultBean.USER_NOT_EXIST, null); } //创建新的评论 Comment comment = new Comment(); comment.setBlogId(blogId); comment.setCommentBody(commentBody); comment.setCreateTime(new Date()); comment.setUserId(user.getId()); comment.setCommentType(CommentConstants.FIRST_COMMENT_TYPE); commentService.save(comment); return Result.success(); } ``` 注意的是,这里**发布评论的时候要求用户是已经存在数据库表tb_user中的,如果需求是任意的游客都可以发布评论的话,那么需要重新设置tb_comment表,将发表评论的用户id直接修改为用户的名字即可**。 ### 根据关键词keyword进行模糊查找 在本次的项目中,根据keyword进行模糊查找,只要title或者contend中包含了keyword,那么就将该blog返回即可。**后续如果需要进行改进的话,这里可以利用Elasticsearch来进行搜索**。