# 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
需要创建数据库:

对应的建表语句为:
```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
);
```
## 后台
### 用户登录/注册

值得注意的是,在前台将数据发送给后台的时候,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;
}
```
登录成功之后,就会来到后台界面,在后台界面中可以看到如下信息:

### 博客管理
博客管理的界面如下所示,主要包括的是新增博客,编辑博客,删除博客等管理。

注意的是,当我们点击了博客管理,或者总文章数的时候,就会来到/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了**。
下面的操作中也是根据类似的道理操作的。
### 评论管理

### 标签管理

### 分类管理

其余的操作暂时还没有实现,后续可能会继续进行完善,例如友情链接,系统配置,但是已经实现了退出,只需要让这个session失效即可。
## 前台
### 前台首页

对应的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,效果如下所示:

### 根据category查看blog
点击某一个分类,例如`admin in 日常随笔`中的日常随笔,就会发送请求`/blog/category?categoryId=xxx`,然后获取数据,在回到`/blag/amaze/list.html`界面.对应的流程为:

对应的代码为:
```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`进行渲染,对应的流程为:

对应的代码为:
```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详情

```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来进行搜索**。