springboot+vue练手级项目,真实的在线博客系统
Posted m0_67403240
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了springboot+vue练手级项目,真实的在线博客系统相关的知识,希望对你有一定的参考价值。
文章目录
- spring boot 练手实战项目说明
- 基础知识
- 面试准备
- 1. 工程搭建
- 2. 首页-文章列表
- 3. 首页-最热标签
- 4.1. 统一异常处理
- 4.2. 首页-最热文章
- 4.3. 首页-最新文章
- 4.4. 首页-文章归档
- 5.1. 登录
- 5.2. 获取用户信息
- 5.3. 退出登录
- 6.1. 注册
- 6.2. 登录拦截器
- 6.3. ThreadLocal保存用户信息
- 7.1. ThreadLocal内存泄漏
- 7.2. 文章详情
- 7.3. 使用线程池 更新阅读次数
- Bug修正
- 8.1. 评论列表
- 8.2. 评论
- 写文章
- 9.1. 所有文章分类
- 9.2. 所有文章标签
- 9.3. 发布文章
- 9.4. AOP日志
- bug修正
- 10.1. 文章图片上传
- 10.2. 导航-文章分类
- 10.3. 分类文章列表
- 10.4. 标签文章列表
- 11.1. 归档文章列表
- 11.2. 统一缓存处理(优化)
- 11.3. 思考别的优化
- 管理后台
- 12.1. 搭建项目
- 12.2. 权限管理
- 12.3. Security集成
- 12.4. 作业
- 13.总结技术亮点
- 14.前端
- 15、缓存一致性问题
spring boot 练手实战项目说明
码神之路网站所使用的博客,项目简单,需求明确,容易上手,非常适合做为练手级项目。
最终成品
项目讲解说明:
- 提供前端工程,只需要实现后端接口即可
- 项目以单体架构入手,先快速开发,不考虑项目优化,降低开发负担
- 开发完成后,开始优化项目,提升编程思维能力
- 比如页面静态化,缓存,云存储,日志等
- docker部署上线
- 云服务器购买,域名购买,域名备案等
项目使用技术 :
springboot + mybatisplus+redis+mysql
代码地址:
个人博客代码地址
基础知识
Spring常用注解,注解 IOC ,AOP,MVC 的理解
mybatisDao层 Mapper层 controller层 service层 model层 entity层 简介
mall商场学习文档
mybatisplus学习文档
mybatisplus配套代码
easycode搭配mybatisplus巨爽
@Autowired 的时候为什么注入接口不是实现类
@Service注解为什么是标注在实现类上的
mapper接口需要加注解吗?通过MapperScan减少注解
@Mapper与@MapperScan注解的作用是什么?
推荐安装插件
配好@Data使用
查看文章代码结构
自动提示编写的代码
快速生成xml文件
区分括号
vscode插件
面试准备
1. 工程搭建
前端的工程:
npm install
npm run build
npm run dev
1.1 新建maven工程
<?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.mszlu</groupId>
<artifactId>blog-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<!-- 排除 默认使用的logback -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/joda-time/joda-time -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.10</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
删除src文件
1.1.2遇到的bug
1.2 配置
数据库配置
配置
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
使其打印到控制台
配置
mybatis-plus.global-config.db-config.table-prefix=ms_
代表标识下前缀是ms_,这样使其省略前缀,不用再定义前缀的表名
#server
server.port= 8888
spring.application.name=mszlu_blog
# datasource
spring.datasource.url=jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#mybatis-plus
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.global-config.db-config.table-prefix=ms_
package com.mszlu.blog.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author ljm
* @Date 2021/10/10 20:11
* @Version 1.0
*/
//让spring可以扫描到它
@Configuration
//扫包,将此包下的接口生成代理实现类,并且注册到spring容器中
@MapperScan("com.mszlu.blog.dao.mapper")
public class MybatisPlusConfig
//分页插件
//@Configuration可理解为用spring的时候xml里面的<beans>标签。
// @Bean可理解为用spring的时候xml里面的<bean>标签。
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor()
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
package com.mszlu.blog.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer
@Override
public void addCorsMappings(CorsRegistry registry)
//跨域配置,不可设置为*,不安全, 前后端分离项目,可能域名不一致
//本地测试 端口不一致 也算跨域
registry.addMapping("/**").allowedOrigins("http://localhost:8080");
1.3 启动类
package com.mszlu.blog;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BlogApp
public static void main(String[] args)
SpringApplication.run(BlogApp.class,args);
2. 首页-文章列表
2.1 接口说明
接口url:/articles
请求方式:POST
请求参数:
参数名称
参数类型
说明
page
int
当前页数
pageSize
int
每页显示的数量
返回数据:
"success": true,
"code": 200,
"msg": "success",
"data": [
"id": 1,
"title": "springboot介绍以及入门案例",
"summary": "通过Spring Boot实现的服务,只需要依靠一个Java类,把它打包成jar,并通过`java -jar`命令就可以运行起来。
这一切相较于传统Spring应用来说,已经变得非常的轻便、简单。",
"commentCounts": 2,
"viewCounts": 54,
"weight": 1,
"createDate": "2609-06-26 15:58",
"author": "12",
"body": null,
"tags": [
"id": 5,
"avatar": null,
"tagName": "444"
,
"id": 7,
"avatar": null,
"tagName": "22"
,
"id": 8,
"avatar": null,
"tagName": "11"
],
"categorys": null
,
"id": 9,
"title": "Vue.js 是什么",
"summary": "Vue (读音 /vju?/,类似于 view) 是一套用于构建用户界面的渐进式框架。",
"commentCounts": 0,
"viewCounts": 3,
"weight": 0,
"createDate": "2609-06-27 11:25",
"author": "12",
"body": null,
"tags": [
"id": 7,
"avatar": null,
"tagName": "22"
],
"categorys": null
,
"id": 10,
"title": "Element相关",
"summary": "本节将介绍如何在项目中使用 Element。",
"commentCounts": 0,
"viewCounts": 3,
"weight": 0,
"createDate": "2609-06-27 11:25",
"author": "12",
"body": null,
"tags": [
"id": 5,
"avatar": null,
"tagName": "444"
,
"id": 6,
"avatar": null,
"tagName": "33"
,
"id": 7,
"avatar": null,
"tagName": "22"
,
"id": 8,
"avatar": null,
"tagName": "11"
],
"categorys": null
]
2.2 编码
2.2.0 Spring—基于注解开发
2.2.1 表结构
文章表
CREATE TABLE `blog`.`ms_article` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`comment_counts` int(0) NULL DEFAULT NULL COMMENT '评论数量',
`create_date` bigint(0) NULL DEFAULT NULL COMMENT '创建时间',
`summary` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '简介',
`title` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '标题',
`view_counts` int(0) NULL DEFAULT NULL COMMENT '浏览数量',
`weight` int(0) NOT NULL COMMENT '是否置顶',
`author_id` bigint(0) NULL DEFAULT NULL COMMENT '作者id',
`body_id` bigint(0) NULL DEFAULT NULL COMMENT '内容id',
`category_id` int(0) NULL DEFAULT NULL COMMENT '类别id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 25 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
标签表
id,文章id,标签id,通过文章id可以间接查到标签id
CREATE TABLE `blog`.`ms_tag` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`article_id` bigint(0) NOT NULL,
`tag_id` bigint(0) NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `article_id`(`article_id`) USING BTREE,
INDEX `tag_id`(`tag_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
用户表
CREATE TABLE `blog`.`ms_sys_user` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`account` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '账号',
`admin` bit(1) NULL DEFAULT NULL COMMENT '是否管理员',
`avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像',
`create_date` bigint(0) NULL DEFAULT NULL COMMENT '注册时间',
`deleted` bit(1) NULL DEFAULT NULL COMMENT '是否删除',
`email` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
`last_login` bigint(0) NULL DEFAULT NULL COMMENT '最后登录时间',
`mobile_phone_number` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号',
`nickname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵称',
`password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',
`salt` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '加密盐',
`status` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '状态',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
entity层
别名: model层 ,domain层
用途: 实体层,用于存放我们的实体类,与数据库中的属性值基本保持一致,实现set和get的方法。
例子:user表的实体User
文章
package com.mszlu.blog.dao.pojo;
import lombok.Data;
@Data
public class Article
public static final int Article_TOP = 1;
public static final int Article_Common = 0;
private Long id;
private String title;
private String summary;
private int commentCounts;
private int viewCounts;
/**
* 作者id
*/
private Long authorId;
/**
* 内容id
*/
private Long bodyId;
/**
*类别id
*/
private Long categoryId;
/**
* 置顶
*/
private int weight = Article_Common;
/**
* 创建时间
*/
private Long createDate;
用户
package com.mszlu.blog.dao.pojo;
import lombok.Data;
@Data
public class SysUser
private Long id;
private String account;
private Integer admin;
private String avatar;
private Long createDate;
private Integer deleted;
private String email;
private Long lastLogin;
private String mobilePhoneNumber;
private String nickname;
private String password;
private String salt;
private String status;
标签
package com.mszlu.blog.dao.pojo;
import lombok.Data;
@Data
public class Tag
private Long id;
private String avatar;
private String tagName;
2.2.2 Controller
controller层。控制器,导入service层,因为service中的方法是我们使用到的,controller通过接收前端传过来的参数进行业务操作,在返回一个指定的路径或者数据表
package com.mszlu.blog.api;
import com.mszlu.blog.dao.pojo.Article;
import com.mszlu.blog.service.ArticleService;
import com.mszlu.blog.vo.Archive;
import com.mszlu.blog.vo.ArticleVo;
import com.mszlu.blog.vo.Result;
import com.mszlu.blog.vo.params.PageParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("articles")
public class ArticleController
@Autowired
private ArticleService articleService;
//Result是统一结果返回
@PostMapping
public Result articles(@RequestBody PageParams pageParams)
//ArticleVo 页面接收的数据
List<ArticleVo> articles = articleService.listArticlesPage(pageParams);
return Result.success(articles);
统一最后的结果
package com.mszlu.blog.vo;
import com.mszlu.blog.dao.pojo.Article;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result
private boolean success;
private Integer code;
private String msg;
private Object data;
public static Result success(Object data)
return new Result(true,200,"success",data);
public static Result fail(Integer code, String msg)
return new Result(false,code,msg,null);
建立与前端交互的Vo文件
package com.mszlu.blog.vo;
import com.mszlu.blog.dao.pojo.ArticleBody;
import com.mszlu.blog.dao.pojo.Category;
import com.mszlu.blog.dao.pojo.SysUser;
import com.mszlu.blog.dao.pojo.Tag;
import lombok.Data;
import java.util.List;
@Data
public class ArticleVo
private Long id;
private String title;
private String summary;
private int commentCounts;
private int viewCounts;
private int weight;
/**
* 创建时间
*/
private String createDate;
private String author;
private ArticleBodyVo body;
private List<TagVo> tags;
private List<CategoryVo> categorys;
2.2.3 Service
service层主要是写业务逻辑方法,service层经常要调用dao层(也叫mapper层)的方法对数据进行增删改查的操作。*
2.2.3.0 解决mapper爆红
建立service接口
package com.mszlu.blog.service;
import com.mszlu.blog.vo.Archive;
import com.mszlu.blog.vo.ArticleVo;
import com.mszlu.blog.vo.params.PageParams;
import java.util.List;
public interface ArticleService
/**
* 分页查询文章列表
* @param pageParams
* @return
*/
Result listArticle(PageParams pageParams);
建立service接口的实现类
package com.mszlu.blog.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.mszlu.blog.dao.ArticleMapper;
import com.mszlu.blog.dao.SysUserMapper;
import com.mszlu.blog.dao.pojo.Article;
import com.mszlu.blog.dao.pojo.SysUser;
import com.mszlu.blog.dao.pojo.Tag;
import com.mszlu.blog.service.ArticleService;
import com.mszlu.blog.service.SysUserService;
import com.mszlu.blog.service.TagsService;
import com.mszlu.blog.vo.ArticleBodyVo;
import com.mszlu.blog.vo.ArticleVo;
import com.mszlu.blog.vo.TagVo;
import com.mszlu.blog.vo.params.PageParams;
import org.joda.time.DateTime;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class ArticleServiceImpl implements ArticleService
@Autowired
private ArticleMapper articleMapper;
@Autowired
private TagService tagService;
@Autowired
private SysUserService sysUserService;
@Override
public Result listArticle(PageParams pageParams)
/**
* 1、分页查询article数据库表
*/
Page<Article> page = new Page<>(pageParams.getPage(), pageParams.getPageSize());
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
//是否置顶进行排序, //时间倒序进行排列相当于order by create_data desc
queryWrapper.orderByDesc(Article::getWeight,Article::getCreateDate);
Page<Article> articlePage = articleMapper.selectPage(page, queryWrapper);
//分页查询用法 https://blog.csdn.net/weixin_41010294/article/details/105726879
List<Article> records = articlePage.getRecords();
// 要返回我们定义的vo数据,就是对应的前端数据,不应该只返回现在的数据需要进一步进行处理
List<ArticleVo> articleVoList =copyList(records,true,true);
return Result.success(articleVoList);
private List<ArticleVo> copyList(List<Article> records,boolean isTag,boolean isAuthor)
List<ArticleVo> articleVoList = new ArrayList<>();
for (Article record : records)
articleVoList.add(copy(record,isTag,isAuthor));
return articleVoList;
//"eop的作用是对应copyList,集合之间的copy分解成集合元素之间的copy
private ArticleVo copy(Article article,boolean isTag,boolean isAuthor)
ArticleVo articleVo = new ArticleVo();
//BeanUtils.copyProperties用法 https://blog.csdn.net/Mr_linjw/article/details/50236279
BeanUtils.copyProperties(article, articleVo);
articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
//并不是所有的接口都需要标签和作者信息
if(isTag)
Long articleId = article.getId();
articleVo.setTags(tagService.findTagsByArticleId(articleId));
if (isAuthor)
//拿到作者id
Long authorId = article.getAuthorId();
articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname());
return articleVo;
建立用户的service接口
package com.mszlu.blog.service;
import com.mszlu.blog.dao.pojo.SysUser;
public interface UserService
SysUser findUserById(Long userId);
建立用户的service接口实现类
package com.mszlu.blog.service.impl;
import com.mszlu.blog.dao.SysUserMapper;
import com.mszlu.blog.dao.pojo.SysUser;
import com.mszlu.blog.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class SysUserServiceImpl implements SysUserService
@Autowired
private SysUserMapper sysUserMapper;
@Override
public SysUser findUserById(Long id)
//根据id查询
//为防止sysUser为空增加一个判断
SysUser sysUser = sysUserMapper.selectById(id);
if (sysUser == null)
sysUser = new SysUser();
sysUser.setNickname("码神之路");
return sysUser;
2.2.3.1mybatisplus遇到多表查询怎么办
TagMapper的建立中遇到这个问题了,办法是在建立到TagMapper后需要建立xml文件进行读写
xml文件放到resource文件夹下
文件夹名和xml文件名必须和TagMapper.java文件夹保持一致
2.2.3.2创建文件夹时遇到的坑
使用IntelliJ IDEA创建多级文件夹时,文件夹名为com.immer.monitor.persistence 和 com/immer/monitor/persistence 均会显示为如下图所示
但实际结构确实截然不同 com.immer.monitor.persistence 是单个文件夹 而 com/immer/monitor/persistence 是一个文件夹嵌套
会导致资源文件not found 的问题,而且很难排查得到
要不会显示找不到文件夹路径的问题,因为我们要保证mapper.xml要和mapper的文件夹和路径保持一致
建立标签的service接口
package com.mszlu.blog.service;
import com.mszlu.blog.dao.pojo.Tag;
import com.mszlu.blog.vo.TagVo;
import java.util.List;
public interface TagsService
List<TagVo> findTagsByArticleId(Long id);
建立tag的service接口的实现类
package com.mszlu.blog.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mszlu.blog.dao.TagMapper;
import com.mszlu.blog.dao.pojo.Tag;
import com.mszlu.blog.service.TagsService;
import com.mszlu.blog.vo.TagVo;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@Service
public class TagsServiceImpl implements TagsService
@Autowired
private TagMapper tagMapper;
/**
* ms_article_tag是文章和标签的关联表
* ms_tag为单纯的标签的表
* @param articleId
* @return
*/
@Override
public List<TagVo> findTagsByArticleId(Long articleId)
//mybatisplus无法进行多表查询
List<Tag> tags = tagMapper.findTagsByArticleId(articleId);
return copyList(tags);
private List<TagVo> copyList(List<Tag> tagList)
List<TagVo> tagVoList = new ArrayList<>();
for (Tag tag : tagList)
tagVoList.add(copy(tag));
return tagVoList;
private TagVo copy(Tag tag)
TagVo tagVo = new TagVo();
BeanUtils.copyProperties(tag, tagVo);
return tagVo;
2.2.4 Dao层
mapper层=dao层,现在用mybatis逆向工程生成的mapper层,其实就是dao层。
dao层对数据库进行数据持久化操作,他的方法语句是直接针对数据库操作的,而service层是针对我们controller,也就是针对我们使用者。service的impl是把mapper和service进行整合的文件。
dao层和service层关系:service层经常要调用dao层的方法对数据进行增删改查的操作,现实开发中,对业务的操作会涉及到数据的操作,而对数据操作常常要用到数据库,所以service层会经常调用dao层的方法。
文章的dao层
由于我们直接继承了mybatisplus的BaseMapper所以基本的增删改查都不用再写了。
package com.mszlu.blog.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mszlu.blog.dao.pojo.Article;
import com.mszlu.blog.vo.ArticleVo;
import java.util.List;
public interface ArticleMapper extends BaseMapper<Article>
标签的dao层
package com.mszlu.blog.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mszlu.blog.dao.pojo.Tag;
import java.util.List;
public interface TagMapper extends BaseMapper<Tag>
List<Tag> findTagsByArticleId(Long articleId);
作者的dao层
package com.mszlu.blog.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mszlu.blog.dao.pojo.SysUser;
public interface SysUserMapper extends BaseMapper<SysUser>
这就是上文提到的mybatisplus遇到多表查询怎么办,我们需要建立自己的xml文件进行联合查询操作
在mapper文件中
利用mapperX插件我们可以创建
方法名为select的命令
<select id="findTagsByArticleId" resultType="com.mszlu.blog.dao.pojo.Tag"></select>
总的xml文件如下所示
<?xml version="1.0" encoding="UTF-8" ?>
<!--MyBatis配置文件-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mszlu.blog.dao.mapper.TagMapper">
<sql id="all">
id,avatar,tag_name as tagName
</sql>
<!-- List<Tag> findTagsByArticleId(Long articleId);
在这个文件中,id代表方法名,parameterType表示输入变量的类型,resultType表示泛型的类型-->
<select id="findTagsByArticleId" parameterType="long" resultType="com.mszlu.blog.dao.pojo.Tag">
select id,avatar,tag_name as tagName from ms_tag
where id in (select tag_id from ms_article_tag where article_id=#articleId)
</select>
</mapper>
2.2.5 测试
3. 首页-最热标签
3.1 接口说明
接口url:/tags/hot
请求方式:GET
请求参数:无
id
标签名称
我们期望点击标签关于文章的所有列表都显示出来
返回数据:
"success": true,
"code": 200,
"msg": "success",
"data": [
"id":1,
"tagName":"4444"
]
3.2 编码
3.2.1 Controller
package com.mszlu.blog.api;
import com.mszlu.blog.service.ArticleService;
import com.mszlu.blog.service.TagsService;
import com.mszlu.blog.vo.Archive;
import com.mszlu.blog.vo.ArticleVo;
import com.mszlu.blog.vo.Result;
import com.mszlu.blog.vo.TagVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
//@RestController代表我们返回的是json数据,@RequestMapping("tags")表示路径映射
@RestController
@RequestMapping("tags")
public class TagsController
@Autowired
private TagService tagService;
// 路径 tags/hot
@GetMapping("hot")
public Result hot()
int limit =6;
return tagService.hots(limit);
vo表示后端与前端交互的数据
package com.mszlu.blog.vo;
import lombok.Data;
@Data
public class TagVo
private Long id;
private String tagName;
3.2.2 Service
建立service接口
package com.mszlu.blog.service;
import com.mszlu.blog.vo.TagVo;
import java.util.List;
public interface TagsService
Result hots(int limit);
建立serviceimpl实现类
package com.mszlu.blog.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mszlu.blog.dao.TagMapper;
import com.mszlu.blog.dao.pojo.Tag;
import com.mszlu.blog.service.TagsService;
import com.mszlu.blog.vo.TagVo;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@Service
public class TagsServiceImpl implements TagsService
@Autowired
private TagMapper tagMapper;
@Override
public Result hots(int limit)
/**
* 最热标签就是对标签ms_article_tag中的tag_id进行排序数量最多的就是我们的最热标签
* 1、标签所拥有的文章数量最多就是最热标签
* 2、查询 根据tag_id分组计数,从大到小排列取前limit个
*/
List<Long> tagIds = tagMapper.findHotsTagIds(limit);
//因为id in(1,2,3) 里面不能为空所以我们需要进行判断
// CollectionUtils.isEmpty作用 https://blog.csdn.net/qq_42848910/article/details/105717235
if(CollectionUtils.isEmpty(tagIds))
return Result.success(Collections.emptyList());
//需求的是tagId 和tagName Tag对象
//我们的sql语句类似于select * from tag where id in (1,2,3)
List<Tag> tagList = tagMapper.findTagsByTagIds(tagIds);
return Result.success(tagList);
3.2.3 Dao
TagMapper.java
package com.mszlu.blog.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mszlu.blog.dao.pojo.Tag;
import java.util.List;
public interface TagMapper extends BaseMapper<Tag>
/**
* 查询最热的标签前n条
* @param limit
* @return
*/
List<Long> findHotsTagIds(int limit);
List<Tag> findTagsByTagIds(List<Long> tagIds);
TagMapper.xml文件
一定要了解所有表的业务逻辑,知道自己要返回什么值再进行操作
我们通过findHotsTagIds这个方法在ms_article_tag表中找到了tag_id
然后多表查询,tag_id就是ms_tag表中的id我们在findHotsTagIds这个方法中找到了我们想要的前两条id,然后再利用动态mysql这个方法将id,tagName两个选项选择出来。多写多看基本上可以成为一个合格的crud工程师
<?xml version="1.0" encoding="UTF-8" ?>
<!--MyBatis配置文件-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mszlu.blog.dao.TagMapper">
<!-- List<Long> findHotsTagIds(int limit);-->
<!-- parameterType="int"是自己加的因为不会自动生成我们输入的标签,#limit为我们自己传的参数-->
<!-- GROUP by 用法 https://www.runoob.com/sql/sql-groupby.html-->
<!-- sql语句的意思是在ms_article_tag表中查找tag_id,根据tag_id将其聚合在一起,再根据count(*)的数量以递减的顺序排序最后限制输出两条数据-->
<select id="findHotsTagIds" parameterType="int" resultType="java.lang.Long">
select tag_id from ms_article_tag GROUP BY tag_id ORDER BY count(*) DESC LIMIT #limit
</select>
<!-- List<Tag> findTagsByTagIds(List<Long> tagIds);因为输入的类型是list所以parameterType的值是list-->
<!-- foreach用法 https://www.cnblogs.com/fnlingnzb-learner/p/10566452.html
相当于for循环找传进来的一个id集合,每个id通过sql语句找到对应的tag对象-->
<select id="findTagsByTagIds" parameterType="list" resultType="com.mszlu.blog.dao.pojo.Tag">
select id,tag_name as tagName from ms_tag
where id in
<foreach collection="tagIds" item="tagId" separator="," open="(" close=")">
#tagId
</foreach>
</select>
</mapper>
3.2.4 测试
最热标签显示出来
4.1. 统一异常处理
不管是controller层还是service,dao层,都有可能报异常,如果是预料中的异常,可以直接捕获处理,如果是意料之外的异常,需要统一进行处理,进行记录,并给用户提示相对比较友好的信息。
package com.mszlu.blog.handler;
import com.mszlu.blog.vo.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
//对加了@controller注解的方法进行拦截处理 Aop的实现
@ControllerAdvice
public class AllExceptionHandler
//进行异常处理,处理Exception.class的异常
@ExceptionHandler(Exception.class)
@ResponseBody //返回json数据如果不加就返回页面了
public Result doException(Exception ex)
//e.printStackTrace();是打印异常的堆栈信息,指明错误原因,
// 其实当发生异常时,通常要处理异常,这是编程的好习惯,所以e.printStackTrace()可以方便你调试程序!
ex.printStackTrace();
return Result.fail(-999,"系统异常");
4.2. 首页-最热文章
在ms_article表中的view_counts表示浏览数量,越多表示越火热
4.2.1 接口说明
接口url:/articles/hot
请求方式:POST
请求参数:
参数名称
参数类型
说明
返回数据:
"success": true,
"code": 200,
"msg": "success",
"data": [
"id": 1,
"title": "springboot介绍以及入门案例",
,
"id": 9,
"title": "Vue.js 是什么",
,
"id": 10,
"title": "Element相关",
]
4.2.2 Controller
ArticleController.java
/**
* 首页最热文章
* @return
*/
@PostMapping("hot")
public Result hotArticle()
int limit = 5;
return articleService.hotArticle(limit);
4.2.3 Service
src/main/java/com/mszlu/blog/service/ArticleService.java
package com.mszlu.blog.service;
import com.mszlu.blog.vo.Result;
import com.mszlu.blog.vo.params.PageParams;
/**
* @Author ljm
* @Date 2021/10/11 10:30
* @Version 1.0
*/
public interface ArticleService
/**
* 分页查询文章列表
* @param pageParams
* @return
*/
Result listArticle(PageParams pageParams);
/**
* 最热文章
* @param limit
* @return
*/
Result hotArticle(int limit);
src/main/java/com/mszlu/blog/service/impl/ArticleServiceImpl.java
@Override
public Result hotArticle(int limit)
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(Article::getViewCounts);
queryWrapper.select(Article::getId,Article::getTitle);
//"limit"字待串后要加空格,不要忘记加空格,不然会把数据拼到一起
queryWrapper.last("limit "+limit);
//select id,title from article order by view_counts desc limt 5
List<Article> articles = articleMapper.selectList(queryWrapper);
//返回vo对象
return Result.success(copyList(articles,false,false));
4.2.4 测试
4.3. 首页-最新文章
和最热文章非常类似,一个是根据浏览量来选择,一个是根据最新创建时间来选择
4.3.1 接口说明
接口url:/articles/new
请求方式:POST
请求参数:
参数名称
参数类型
说明
返回数据:
"success": true,
"code": 200,
"msg": "success",
"data": [
"id": 1,
"title": "springboot介绍以及入门案例",
,
"id": 9,
"title": "Vue.js 是什么",
,
"id": 10,
"title": "Element相关",
]
4.3.1 Controller
/**
* 首页 最新文章
* @return
*/
@PostMapping("new")
public Result newArticles()
int limit = 5;
return articleService.newArticles(limit);
4.3.2 Service
src/main/java/com/mszlu/blog/service/ArticleService.java
/**
* 最新文章
* @param limit
* @return
*/
Result newArticles(int limit);
src/main/java/com/mszlu/blog/service/impl/ArticleServiceImpl.java
@Override
public Result newArticles(int limit)
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(Article::getCreateDate);
queryWrapper.select(Article::getId,Article::getTitle);
queryWrapper.last("limit "+limit);
//select id,title from article order by create_date desc limit 5
List<Article> articles = articleMapper.selectList(queryWrapper);
return Result.success(copyList(articles,false,false));
4.4. 首页-文章归档
每一篇文章根据创建时间某年某月发表多少篇文章
4.4.1接口说明
接口url:/articles/listArchives
请求方式:POST
请求参数:
参数名称
参数类型
说明
返回数据:
"success": true,
"code": 200,
"msg": "success",
"data": [
"year": "2021",
"month": "6",
"count": 2
]
? select year(create_date) as year,month(create_date) as month,count(*) as count from ms_article group by year,month
但是
p9 up主给的sql里面create_date 为bigint 13位,直接year()不行,需要先转date型后year()。
select year(FROM_UNIXTIME(create_date/1000)) year,month(FROM_UNIXTIME(create_date/1000)) month, count(*) count from ms_article group by year,month;
这样才能查出来结果
4.4.1 Controller
src/main/java/com/mszlu/blog/controller/ArticleController.java
/**
* 首页 文章归档
* @return
*/
@PostMapping("listArchives")
public Result listArchives()
return articleService.listArchives();
下面这个是在src/main/java/com/mszlu/blog/service/impl/ArticleServiceImpl.java中使用的返回值
package com.mszlu.blog.dao.dos;
import lombok.Data;
/**
* @Author ljm
* @Date 2021/10/12 17:19
* @Version 1.0
* do 对象 数据库 查询出来的对象但是不需要持久化,由于do是关键字所以加了个s成为dos
*/
@Data
public class Archives
private Integer year;
private Integer month;
private Long count;
4.4.2 Service
src/main/java/com/mszlu/blog/service/ArticleService.java
/**
* 文章归档
* @return
*/
Result listArchives();
src/main/java/com/mszlu/blog/service/impl/ArticleServiceImpl.java
@Override
public Result listArchives()
/*
文章归档
*/
List<Archives> archivesList = articleMapper.listArchives();
return Result.success(archivesList);
4.4.3 Dao
src/main/java/com/mszlu/blog/dao/mapper/ArticleMapper.java
package com.mszlu.blog.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mszlu.blog.dao.pojo.Article;
import java.util.List;
import java.util.Map;
public interface ArticleMapper extends BaseMapper<Article>
List<Archives> listArchives();
src/main/resources/com/mszlu/blog/dao/mapper/ArticleMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!--MyBatis配置文件-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--创立ArticleMapper.xml文件后再利用mybatisX一键生成select语句-->
<mapper namespace="com.mszlu.blog.dao.mapper.ArticleMapper">
<select id="listArchives" resultType="com.mszlu.blog.dao.dos.Archives">
select year(FROM_UNIXTIME(create_date/1000)) as year,month(FROM_UNIXTIME(create_date/1000)) as month, count(*) as count from ms_article
group by year,month
</select>
</mapper>
4.4.4 测试
注意:前端工程 需使用当天资料下的app
5.1. 登录
5.1.1 接口说明
接口url:/login
请求方式:POST
请求参数:
参数名称
参数类型
说明
account
string
账号
password
string
密码
返回数据:
"success": true,
"code": 200,
"msg": "success",
"data": "token"
5.1.2 JWT
登录使用JWT技术。
jwt 可以生成 一个加密的token,做为用户登录的令牌,当用户登录成功之后,发放给客户端。
请求需要登
以上是关于springboot+vue练手级项目,真实的在线博客系统的主要内容,如果未能解决你的问题,请参考以下文章
手撸一个基于Springboot+Vue的书籍论坛系统,可用于课程设计和毕业设计或者练手
手撸一个基于Springboot+Vue的书籍论坛系统,可用于课程设计和毕业设计或者练手