一、项目介绍
个人博客系统
- 相关技术: SpringBoot+SpringMvc+Mybatis+Mysql+Redis
- 项目简介:本项目为一个功能完善的个人博客系统,支持文章的编辑、修改、删除和发布,以及作者个人信息的展示等功能。
- 项目描述:
- 采用前后端分离架构,基于SpringBoot和Mybatis等框架构建。
- 设计统一的前后端数据返回格式,提高系统可读性和可维护性。
- 使用拦截器实现用户登录校验,并采用MD5算法对密码进行加密存储。
- 利用Redis对内容数据进行缓存,减轻数据库压力,提高系统性能。
- 使用ThreadPoolExecutor线程池技术,并发执行查询文章详情和修改文章阅读量等任务,提升系统响应速度。
二、项目实现效果
项目包含7个前端页面(注册,登录,个人博客,总博客,修改已有博客,增加新博客,显示博客详情),每个页面顶端有一个导航栏。其中编辑文章使用markdown编辑器,不过其中个人信息没有实现修改功能,其头像和博客地址固定了(显示为奶龙和我的gitee地址)。
注册页面:
登录页面:
个人博客页面:
总博客页面:
修改已有博客页面:
增加新博客页面:
显示博客详情页面(左边有作者个人信息):
三、项目具体实现
1.软件生命周期
一个软件的生命周期可以划分为
- 可行性研究
- 需求分析
- 概要设计
- 详细设计
- 编码实现
- 测试
- 使用及维护
- 退役
2.项目需求分析
- 用户 注册、登录、退出登录 的功能。
- 显示个人博客列表:按发布时间倒序排列,各博客显示 标题、发布时间、简介。博客下方有 查看全文、修改、删除等功能。
- 显示总博客列表:分页显示(首页、末页、上一页、下一页功能),按发布时间倒序排列,各博客显示 标题、发布时间、简介。博客下方有 查看全文 功能。
- 查看全文:显示作者信息(头像,账号名,博客地址,所发布文章总数),博客信息(标题,发布时间,文章详情,文章阅读量)。
- 增加新文章功能。
- 修改已有文章功能(在修改界面,导入文章之前已有的内容)。
- 删除文章功能。
- 用户权限限制:进入总博客页面,查看全文,无需登录。进入个人博客页面(修改,删除博客),增加新博客,都需要登录(如果用户没有登录,则强制登录)。
3.设计
主要是设计数据库存储用户和文章信息。
用户信息表结构:
表中的 昵称 未作具体使用,博客地址,头像 固定了不可修改。
文章信息表结构:
文章发布和修改时间自动为当前时间。
4.编码实现
4.1项目构建及相关配置
基于SpringBoot和Mybatis等框架构建项目,及mysql,Mybatis,Redis等相关配置...
4.2创建实体类(依据数据库中表结构)
例如:
@Data
public class Userinfo implements Serializable {
private int id;
private String username;
private String password;
private String nickname;
private String blogSite;
private String photo;
private boolean state;
}
4.3数据库持久层(涉及到的增删查改)
例如:
public interface ArticleMapper {
//添加文章
@Insert("insert into articleinfo(title,content,author_id,intro) values(#{title},#{content},#{author_id},#{intro})")
public int addArticle(Articleinfo articleinfo);
//以用户id获取用户文章列表
@Select("select * from articleinfo where author_id=#{author_id} order by id desc")
public List<Articleinfo> getUserArticleList(@Param("author_id") int author_id);
//以用户id和文章id删除文章
@Delete("delete from articleinfo where author_id=#{author_id} and id=#{id}")
public int deleteArticle(@Param("author_id")int author_id,@Param("id") int id);
//根据用户id和文章id得到文章
@Select("select * from articleinfo where author_id=#{author_id} and id=#{id}")
public Articleinfo getArticleByUidAid(@Param("author_id")int author_id,@Param("id") int id);
//修改文章
@Update("update articleinfo set title=#{title},content=#{content},intro=#{intro} where id=#{id}")
public int updateArticle(Articleinfo articleinfo);
//根据文章id查询到文章
@Select("select * from articleinfo where id=#{id}")
public Articleinfo getArticleByAid(@Param("id") int id);
//统计用户发布文章数量
@Select("select count(*) from articleinfo where author_id=#{author_id}")
public int getArticleCountByUid(@Param("author_id")int author_id );
//修改文章阅读量,+1.
@Update("update articleinfo set read_count=read_count+1 where id=#{id}")
public int updateArticleReadCount(@Param("id") int id);
//查询所有文章,分页.
@Select("select * from articleinfo order by id desc limit #{perPage} offset #{offsetPage}")
public List<Articleinfo> getAllArticles(@Param("perPage") int perPage,@Param("offsetPage") int offsetPage);
//计算一下总文章数
@Select("select count(*) from articleinfo ")
public int getAllArticleCount();
4.4统一前后端数据交互对象
public class ResultAjax {
private int code;
private String msg;
private Object data;
public static ResultAjax successful1(Object data){
ResultAjax result=new ResultAjax();
result.setCode(200);
result.setData(data);
result.setMsg("");
return result;
}
public static ResultAjax successful2(int code,String msg,Object data){
ResultAjax result=new ResultAjax();
result.setCode(code);
result.setData(data);
result.setMsg(msg);
return result;
}
public static ResultAjax fail1(int code,String msg){
ResultAjax result=new ResultAjax();
result.setCode(code);
result.setMsg(msg);
return result;
}
public static ResultAjax fail2(int code,String msg,Object data){
ResultAjax result=new ResultAjax();
result.setCode(code);
result.setData(data);
result.setMsg(msg);
return result;
}
}
4.5注册、登录、退出登录
采用MD5算法对密码进行加密存储:
存储密码:利用uuid生成唯一的字符串作为盐值,与用户注册时输入的密码拼接后MD5加密生成最终密码,在数据库中存储盐值+最终密码。
验证密码:将数据库中盐值与最终密码分开, 将用户登录时输入的待验证密码与盐值进行MD5加密后,再与最终密码比较.
public class PasswordUtils {
//加密
public static String encrypt(String password){
//1.用uuid作为盐值
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 verifyPassword(String password,String databasePassword){
//1.将盐值与最终密码分开
String[] saltPassword=databasePassword.split("\\$");
//2.将待验证密码与盐值进行md5加密,再与最终密码比较.
String salt=saltPassword[0];
String finalPassword =saltPassword[1];
String verifyPassword= DigestUtils.md5DigestAsHex((salt+password).getBytes(StandardCharsets.UTF_8));
if(verifyPassword.equals(finalPassword)){
return true;
}else {
return false;
}
}
}
登录后利用Session(会话)存储登录信息:
//得到当前登录用户对象
public class SessionUtils {
public static Userinfo getLoginUser(HttpServletRequest request){
HttpSession session=request.getSession(false);
if(session!=null && session.getAttribute(SESSION_KEY)!=null){
return (Userinfo) session.getAttribute(SESSION_KEY);
}
return null;
}
}
4.6显示个人博客列表
根据用户id查询该作者所有文章,按时间倒序(也可以按文章id倒序排列)显示。
4.7显示总博客列表
要根据文章总数来计算总页数,和分页查询显示当前页面:利用ThreadPoolTaskExecutor线程池,并发执行分页查询数据库中文章和统计所有文章数(两个都是查询操作可以并发执行)每页显示文章数,由前端传入(默认每页显示两条文章)。
//查询所有文章,分页.
@RequestMapping("/getAllArticles")
public ResultAjax getAllArticles(Integer currentPage,Integer perPage) throws ExecutionException, InterruptedException {
//无需验证登录也可访问总博客页.
//1.校验参数
if(currentPage==null||currentPage<=0||perPage==null ||perPage<=0){
return ResultAjax.fail1(-1,"参数错误");
}
//2.查询数据库.分页查询limit perPage offset (currentPage-1)*perPage,task1.
int offsetPage=(currentPage-1)*perPage;
//计算一下总文章数,再来计算总页数,task2.
int allArticleCount=0;
//这两个都是查询,可以并发执行.
FutureTask<List<Articleinfo>> task1 = new FutureTask(() -> {
return articleService.getAllArticles(perPage,offsetPage);
});
taskExecutor.submit(task1);
FutureTask<Integer> task2 = new FutureTask(() -> {
return articleService.getAllArticleCount();
});
taskExecutor.submit(task2);
List<Articleinfo> list=task1.get();
allArticleCount= task2.get();
int pageSzie=1;
if(allArticleCount!=0){
pageSzie=allArticleCount/perPage;
if(allArticleCount%perPage!=0){
pageSzie+=1;
}
}
HashMap<String,Object> hashMap=new HashMap<>();
hashMap.put("articleList",list);
hashMap.put("pageSize",pageSzie);
return ResultAjax.successful1(hashMap);
}
}
4.8查看全文
显示文章和用户信息:根据文章id查询到文章,再拿到用户id,从而查询到用户信息。其中(查询用户对象和统计该用户发布文章数量 并且修改当前文章阅读量+1,以上三个操作利用ThreadPoolTaskExecutor线程池并发执行):
//查看全文
//返回文章和用户对象.
@RequestMapping("/fullText")
public ResultAjax fullText(Integer id) throws ExecutionException, InterruptedException {
//1.校验参数
if(id==null || id<=0){
return ResultAjax.fail1(-1,"参数错误");
}
//2.根据文章id查询到文章,再从中拿到用户id.
Articleinfo articleinfo=articleService.getArticleByAid(id);
if(articleinfo==null){
return ResultAjax.fail1(-2,"查看全文失败");
}
int uid=articleinfo.getAuthor_id();
if(uid<=0){
return ResultAjax.fail1(-2,"查看全文失败");
}
// 得到相应用户对象和统计发布文章数量
// 并且修改文章阅读量,+1.
//3.以上三个操作并发执行,利用线程池.
//根据uid查询用户对象
FutureTask<UserinfoVO> userTask = new FutureTask(() -> {
return userService.getUserByUserId(uid);
});
taskExecutor.submit(userTask);
//统计用户发布文章数量
FutureTask<Integer> articleCountTask = new FutureTask(() -> {
return articleService.getArticleCountByUid(uid);
});
taskExecutor.submit(articleCountTask);
//修改文章阅读量,+1.
FutureTask <Integer> articleReadCountTask = new FutureTask(() -> {
return articleService.updateArticleReadCount(id);
});
taskExecutor.submit(articleReadCountTask );
UserinfoVO userinfovo=userTask.get();
int articleCount=articleCountTask.get();
int articleReadCount=articleReadCountTask.get();//返回更新操作影响的行数
//校验一下参数
if(userinfovo==null || articleCount<=0 || articleReadCount !=1){
return ResultAjax.fail1(-2,"查看全文失败");
}
//4.组装好数据
userinfovo.setArticleCount(articleCount);//设好文章发布数量
articleinfo.setRead_count(articleinfo.getRead_count()+1);//文章阅读量+1.
//5.用一个哈希表返回
HashMap<String,Object> hashMap=new HashMap<>();
hashMap.put("user",userinfovo);
hashMap.put("article",articleinfo);
return ResultAjax.successful1(hashMap);
}
4.9增加新文章
增加文章操作...
4.10修改文章
修改文章操作:先查询导入原有文章内容,还要验证该文章是否为当前作者的文章后才能修改(通过比较当前登录用户id与文章用户id)。
4.12删除文章
此处删除文章操作是真的删除数据库中文章...
4.13用户权限限制
使用拦截器实现用户登录校验:
进入总博客页面,查看全文,无需登录。进入个人博客页面(修改,删除博客),增加新博客,需要登录(如果用户没有登录,则强制登录)
/**
* 登录拦截器
*/
public class LoginIntercept implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session=request.getSession(false);
if(session!=null && session.getAttribute(SESSION_KEY)!=null){
return true;
}
//没登录就跳转到登录页面
response.sendRedirect("/login.html");
return false;
}
}
/**
* 系统配置
*/
@Configuration
public class MyConfig implements WebMvcConfigurer {
// 添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginIntercept())
.addPathPatterns("/**") // 拦截所有接⼝
.excludePathPatterns("/editor.md/*")//放行
.excludePathPatterns("/img/*")
.excludePathPatterns("/js/*")
.excludePathPatterns("/css/*")
.excludePathPatterns("/blog_list.html")
.excludePathPatterns("/article/getAllArticles")
.excludePathPatterns("/blog_content.html")
.excludePathPatterns("/article//fullText")
.excludePathPatterns("/reg.html")
.excludePathPatterns("/user/reg")
.excludePathPatterns("/login.html")
.excludePathPatterns("/user/login");
}
}
四、项目代码(gitee地址)
blog_system · new林/项目 - 码云 - 开源中国 (gitee.com)
(服务器过期了,没部署...,redis没用到...)