【springBoot】博客系统

SSM版本的博客系统

1. 项目亮点

  1. 使用MD5+加盐算法进行密码的加密
  2. 使用Redis持久化存储Session
  3. 使用拦截器验证用户登录

2. 项目创建

1.项目框架的选择

image-20231022135548585

2. 项目依赖的引入

image-20231022135718849

3. 静态页面的代码文件:

program/博客系统(静态页面).rar · 叁伍/java语言练习 - 码云 - 开源中国 (gitee.com)

4. 配置文件的书写:

# 配置数据库的连接字符串
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mycnblog?characterEncoding=utf8
    username: root
    password: abc123
    driver-class-name: com.mysql.cj.jdbc.Driver
# 设置 Mybatis 的 xml 保存路径
mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml
  configuration: # 配置打印 MyBatis 执行的 SQL
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 配置打印 MyBatis 执行的 SQL
logging:
  level:
    com:
      example:
        demo: debug

image-20231022141225593

5. 初始化数据库

-- 创建数据库
drop database if exists mycnblog_ssm;
create database mycnblog_ssm DEFAULT CHARACTER SET utf8mb4;

-- 使用数据数据
use mycnblog_ssm;

-- 创建表[用户表]
drop table if exists  userinfo;
create table userinfo(
    id int primary key auto_increment,
    username varchar(100) not null unique,
    password varchar(65) not null,
    photo varchar(500) default '',    
    createtime timestamp default current_timestamp,
    updatetime datetime not null,
    `state` int default 1
) default charset 'utf8mb4';

-- 创建文章表
drop table if exists  articleinfo;
create table articleinfo(
    id int primary key auto_increment,
    title varchar(100) not null,
    content text not null,
    createtime timestamp default current_timestamp,
    updatetime datetime not null,
    uid int not null,
    rcount int not null default 1,
    `state` int default 1
)default charset 'utf8mb4';
-- 添加一个用户信息
INSERT INTO `mycnblog_ssm`.`userinfo` (`id`, `username`, `password`, `photo`, `createtime`, `updatetime`, `state`) VALUES 
(1, 'admin', 'admin', '', '2021-12-06 17:10:48', '2021-12-06 17:10:48', 1);

-- 文章添加测试数据
insert into articleinfo(title,content,uid, `createtime`, `updatetime`)
    values('Java','Java正文',1, '2021-12-06 17:10:48', '2021-12-06 17:10:48');

6. 项目分层

image-20231022145733864

7. 统一数据的返回

统一数据返回,在common包中:

@Data
public class AjaxResult implements Serializable {
    // 状态码
    private Integer code;
    // 状态码描述信息
    private String msg;
    // 返回的数据
    private Object data;
    /**
     * 操作成功返回的结果
     */
    public static AjaxResult success(Object data) {
        AjaxResult result = new AjaxResult();
        result.setCode(200);
        result.setMsg("");
        result.setData(data);
        return result;
    }

    public static AjaxResult success(int code, Object data) {
        AjaxResult result = new AjaxResult();
        result.setCode(code);
        result.setMsg("");
        result.setData(data);
        return result;
    }

    public static AjaxResult success(int code, String msg, Object data) {
        AjaxResult result = new AjaxResult();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }

    /**
     * 返回失败结果
     */
    public static AjaxResult fail(int code, String msg) {
        AjaxResult result = new AjaxResult();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(null);
        return result;
    }

    public static AjaxResult fail(int code, String msg, Object data) {
        AjaxResult result = new AjaxResult();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }

}

保底策略:在返回数据之前,检测数据的类型是否为统一的对象,如果不是,封装成统一的对象。在config包中:

@ControllerAdvice//表示控制器通知类
public class ResponseAdvice implements ResponseBodyAdvice {
    @Autowired
    private ObjectMapper objectMapper;
    /**
     * 开关,如果是true才会调用beforeBodyWrite
     */
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }
    /**
     * 对数据格式进行效验和封装
     */

    @SneakyThrows//异常抛出,相当于方法上throw一个异常
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof AjaxResult){//进行了统一对象的封装,直接返回body
            return body;
        }
        if (body instanceof String){//字符串类型特殊处理。手动对其进行ajax类型的转化
                return objectMapper.writeValueAsString(AjaxResult.success(body));
        }
        return AjaxResult.success(body);//未进行封装的对其进行统一封装
    }
}

3. 实现注册功能

1. 前端实现

前端是在reg.html页面进行实现注册页面的。

<div class="login-container">
    <!-- 中间的注册框 -->
    <div class="login-dialog">
		<!-- …… -->
        <div class="row">
            <button id="submit" onclick="mysub()">提交</button>
        </div>
    </div>
</div>

<script>
    //提交注册事件
    function mysub(){
        // 1.非空效验
        var username = jQuery("#username");
        var password = jQuery("#password");
        var password2 = jQuery("#password2");
        if(username.val()==""){
            alert("请先输入用户名!");
            username.focus(); // 将鼠标光标设置到用户名控件
            return;
        }
        if(password.val()==""){
            alert("请先输入密码!");
            password.focus();
            return;
        }
        if(password2.val()==""){
            alert("请先输入确认密码!");
            password2.focus();
            return;
        }
        // 2.判断两次密码是否一致
        if(password.val()!=password2.val()){
            alert("两次密码输入的不一致,请先检查!");
            password.focus();
            return;
        }
        // 3.ajax 提交请求
        jQuery.ajax({
            url:"/user/reg",//请求地址
            type:"POST",//请求类型
            data:{"username":username.val(),"password":password.val()},//请求数据
            success:function(result){
                // 响应的结果
                if(result!=null && result.code==200 && result.data==1){
                    // 执行成功
                    if(confirm("恭喜:注册成功!是否要跳转到登录页面?")){
                        location.href = "/login.html";
                    }
                }else{
                    alert("抱歉执行失败,请稍后再试!");
                }
            }
        });
    }
</script>

2. 后端实现

Userinfo实体类的实现(在entity包中实现):

@Data
public class Userinfo {
    private Integer id;
    private String username;
    private String password;
    private String photo;
    private LocalDateTime createtime;
    private LocalDateTime updatetime;
    private Integer state;
}

在mapper包中定义UserMapper接口:

@Mapper
public interface UserMapper {
    // 注册
    int reg(Userinfo userinfo);
}

在resources中的mapper文件夹中创建 UserMapper.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
    <insert id="reg">
        insert into userinfo(username,password,updatetime) values(#{username},#{password},#{updatetime})
    </insert>

</mapper>

在 service包中创建UserService类

@Service
public class UserService {
    @Resource
    private UserMapper userMapper;

    public int reg(Userinfo userinfo) {
        return userMapper.reg(userinfo);
    }
}

在 controller包中创建UserController类

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;
    @RequestMapping("/reg")
    public AjaxResult reg(Userinfo userinfo) {
        // 非空效验和参数有效性效验
        if (userinfo == null || !StringUtils.hasLength(userinfo.getUsername()) ||
                !StringUtils.hasLength(userinfo.getPassword())) {
            return AjaxResult.fail(-1, "非法参数");
        }
        userinfo.setUpdatetime(LocalDateTime.now());//设置更新时间
        return AjaxResult.success(userService.reg(userinfo));
    }
}

前端注册成功,并且数据库userinfo表中添加了一条对应的数据,证明注册模块已经实现成功

4. 登录功能

前端页面

<div class="login-container">
    <!-- 中间的注册框 -->
    <div class="login-dialog">
		<!-- …… -->
        <div class="row">
            <button id="submit" onclick="mysub()">提交</button>
        </div>
    </div>
</div>
<script>
        function mysub(){
            // 1.非空效验
            var username = jQuery("#username");
            var password = jQuery("#password");
            if(username.val()==""){
                alert("请先输入用户名!");
                username.focus();
                return;
            }
            if(password.val()==""){
                alert("请先输入密码!");
                password.focus();
                return;
            }
            // 2.ajax 请求登录接口
            jQuery.ajax({
                url:"/user/login",
                type:"POST",
                data:{"username":username.val(),"password":password.val()},
                success:function(result){
                    if(result!=null && result.code==200 && result.data!=null){
                        // 登录成功
                        location.href = "myblog_list.html";
                    }else{
                        alert("抱歉登录失败,用户名或密码输入错误,请重试!");
                    }
                }
            });
        }

</script>

后端实现

在userMapper接口中定义抽象方法:

Userinfo getUserByName(@Param("username") String username);

在 UserMapper.xml文件中添加sql语句:

<select id="getUserByName" resultType="com.example.demo.entity.Userinfo">
    select * from userinfo where username=#{username}
</select>

在userService类中实现getUserByName方法:

public Userinfo getUserByName(String username){
    return userMapper.getUserByName(username);
}

在UserController类中实现login方法:

实现这方法的时候,我们存储用户登录信息在session中。我们session的key值后续在很多地方都会用到,我们在common包中定义一个全局变量

public class AppVariable {
    // 用户 session key
    public static final String USER_SESSION_KEY = "USER_SESSION_KEY";
}

实现login方法:

    @RequestMapping("/login")
    public AjaxResult login(HttpServletRequest request,String username,String password){
        // 非空效验和参数有效性效验
        if (username == null || !StringUtils.hasLength(username) ||
                !StringUtils.hasLength(password) ){
            return AjaxResult.fail(-1, "非法参数");
        }
        // 2.查询数据库
        Userinfo userinfo = userService.getUserByName(username);
        if (userinfo != null && userinfo.getId() > 0) {   // 有效的用户
            // 两个密码是否相同
            if (password.equals(userinfo.getPassword())) {
                // 登录成功
                // 将用户存储到 session
                HttpSession session = request.getSession();
                session.setAttribute(AppVariable.USER_SESSION_KEY, userinfo);
                userinfo.setPassword(""); // 返回前端之前,隐藏敏感(密码)信息

                return AjaxResult.success(userinfo);
            }
        }
        return AjaxResult.success(0,null);
    }

登录拦截器的实现

在config包中,实现切面类和定义拦截规则

实现切面类:

public class LoginInterceptor implements HandlerInterceptor {
    /**
     * true -> 用户已登录
     * false -> 用户未登录
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute(AppVariable.USER_SESSION_KEY) != null) {
            // 用户已登陆
            System.out.println("当前登录用户为:" +
                    ((Userinfo) session.getAttribute(AppVariable.USER_SESSION_KEY)).getUsername());
            return true;
        }
        // 调整到登录页面
        response.sendRedirect("/login.html");
        return false;
    }
}

实现拦截规则:

@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**")
                .excludePathPatterns("/editor.md/**")
                .excludePathPatterns("/img/**")
                .excludePathPatterns("/js/**")
                .excludePathPatterns("/login.html")
                .excludePathPatterns("/reg.html")
                .excludePathPatterns("/blog_list.html")
                .excludePathPatterns("/blog_content.html")
                .excludePathPatterns("/art/detail")
                .excludePathPatterns("/art/incr-rcount")
                .excludePathPatterns("/user/getuserbyid")
                .excludePathPatterns("/art/listbypage")
                .excludePathPatterns("/user/login")
                .excludePathPatterns("/user/reg");
    }
}

5.博客列表页的实现

1. 显示当前用户信息

前端实现
<!-- 版心 -->
<div class="container">
    <!-- 左侧个人信息 -->
    <div class="container-left">
        <div class="card">
            <img src="img/doge.jpg" class="avtar" alt="">
            <h3 id="username"></h3>
            <a href="http:www.github.com">github 地址</a>
            <div class="counter">
                <span>文章</span>
                <span>分类</span>
            </div>
            <div class="counter">
                <span id="artCount"></span>
                <span>1</span>
            </div>
        </div>
    </div>
    <!-- 右侧内容详情 -->
</div>
<script>
    // 获取左侧个人信息
    function showInfo(){
        jQuery.ajax({
            url:"/user/showinfo",
            type:"POST",
            data:{},
            success:function(result){
                if(result!=null && result.code==200){
                    jQuery("#username").text(result.data.username);
                    jQuery("#artCount").text(result.data.artCount);
                }else{
                    alert("个人信息加载失败,请重新刷新再试!");
                }
            }
        });
    }
    showInfo();
</script>
后端实现

在mapper包中创建ArticleMapper接口,并且创建getArtCountByUid抽象方法。

@Mapper
public interface ArticleMapper {
    //查询文章总数
    int getArtCountByUid(@Param("uid") Integer uid);
}

在static的mapper包中创建ArticleMapper.xml文件,书写下面信息

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.ArticleMapper">
    <select id="getArtCountByUid" resultType="Integer">
        select count(*) from articleinfo where uid=#{uid}
    </select>
</mapper>

在service包中创建ArticleService类并且在getArtCountByUid方法中调用articleMapper中的getArtCountByUid方法

@Service
public class ArticleService{
    @Resource
    private ArticleMapper articleMapper;

    public int getArtCountByUid(Integer uid) {
        return articleMapper.getArtCountByUid(uid);
    }
}

在entity包中定义 UserinfoVO继承Userinfo类,新增artCount属性用来存储查询出的文章数量

@Data
public class UserinfoVO extends Userinfo {
    private Integer artCount; // 此人发表的文章总数
}

把对当前登录用户相关的操作进行用一个类进行封装

public class UserSessionUtils {
    /**
     * 得到当前的登录用户
     */
    public static Userinfo getUser(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session != null &&
                session.getAttribute(AppVariable.USER_SESSION_KEY) != null) {
            // 说明用户已经正常登录
            return (Userinfo) session.getAttribute(AppVariable.USER_SESSION_KEY);
        }
        return null;
    }
}

在controller包中的UserController类中定义showInfo方法

@RequestMapping("/showinfo")
public AjaxResult showInfo(HttpServletRequest request) {
    UserinfoVO userinfoVO = new UserinfoVO();
    // 1.得到当前登录用户(从 session 中获取)
    Userinfo userinfo = UserSessionUtils.getUser(request);
    if (userinfo == null) {
        return AjaxResult.fail(-1, "非法请求");
    }
    // Spring 提供的深克隆方法
    BeanUtils.copyProperties(userinfo, userinfoVO);
    // 2.得到用户发表文章的总数
    userinfoVO.setArtCount(articleService.getArtCountByUid(userinfo.getId()));
    return AjaxResult.success(userinfoVO);
}

也可以对1号账户新增一片博客进行验证

insert into articleinfo (title,content,updatetime,uid) value ('第二篇文章','第二篇文章,大家好','2021-12-06 17:10:48',1);

2. 获取文章列表的数据

获取自己的文章列表不用传递参数,后端通过session获取用户id。如果传参可能被有心人通过传参获取别人的文章。

前端实现
// 获取我的文章列表数据
function getMyArtList(){
    jQuery.ajax({
        url:"/art/mylist",
        type:"POST",
        data:{},
        success:function(result){
            if(result!=null && result.code==200){
                // 有两种情况,一种是发表了文章,一种是没有发表任何文章 
                if(result.data!=null && result.data.length>0){
                    // 此用户发表文章了
                    var artListDiv ="";
                    for(var i=0;i<result.data.length;i++){
                        var artItem = result.data[i];
                        artListDiv += '<div class="blog">';
                        artListDiv += '<div class="title">'+artItem.title+'</div>';
                        artListDiv += '<div class="date">'+artItem.updatetime+'</div>';
                        artListDiv += '<div class="desc">';
                        artListDiv += artItem.content;
                        artListDiv += '</div>';
                        artListDiv += '<a href="blog_content.html?id='+
                            artItem.id + '" class="detail">查看全文 &gt;&gt;</a>&nbsp;&nbsp;';
                        artListDiv += '<a href="blog_edit.html?id='+
                            artItem.id + '" class="detail">修改 &gt;&gt;</a>&nbsp;&nbsp;';
                        artListDiv += '<a href="javascript:myDel('+
                            artItem.id+');" class="detail">删除 &gt;&gt;</a>';
                        artListDiv += '</div>';
                    }
                    jQuery("#artDiv").html(artListDiv);
                }else{
                    // 当前用户从未发过任何文章
                    jQuery("#artDiv").html("<h3>暂无文章~</h3>");
                }
            }else{
                alert("查询文章列表出错,请重试!");
            }
        }
    });
}
getMyArtList();
// 删除文章
function myDel(id){

}

上述完成后右侧内容详情删除详情页的信息,留下以下内容:

<!-- 右侧内容详情 -->
<div id="artDiv" class="container-right">
    <!-- 每一篇博客包含标题, 摘要, 时间 -->
</div>
后端实现

在entity包中定义一个Articleinfo实体类

@Data
public class Articleinfo {
    private Integer id;
    private String title;
    private String content;
    private LocalDateTime createtime;
    private LocalDateTime updatetime;
    private Integer uid;
    private Integer rcount;
    private Integer state;
}

在mapper包中的ArticleMapper接口中定义getMyList方法

List<Articleinfo> getMyList(@Param("uid") Integer uid);

在static中的mapper包中的ArticleMapper.xml文件中定义书写内容

<select id="getMyList" resultType="com.example.demo.entity.Articleinfo">
    select * from articleinfo where uid=#{uid}
</select>

在service包中的ArticleService接口中定义getMyList方法

public List<Articleinfo> getMyList(Integer uid) {
    return articleMapper.getMyList(uid);
}

在controller包中定义ArticleController类并且在内部定义getMyList方法

@RestController
@RequestMapping("/art")
public class ArticleController {
    @Autowired
    private ArticleService articleService;

    @RequestMapping("/mylist")
    public AjaxResult getMyList(HttpServletRequest request) {
        Userinfo userinfo = UserSessionUtils.getUser(request);
        if (userinfo == null) {
            return AjaxResult.fail(-1, "非法请求");
        }
        List<Articleinfo> list = articleService.getMyList(userinfo.getId());
        return AjaxResult.success(list);
    }
}

设置返回时间格式:

  1. 在yml中设置全局配置
spring:
  jackson:
    date-format: 'yyyy-MM-dd HH:mm:ss'
    time-zone: 'GMT+8'

这种方式的问题是针对LocalDateTime不起作用,对Date变量才起作用

  1. 使用@JsonFormat注解来返回时间格式
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDateTime createtime;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDateTime updatetime;

6. 注销和删除博客功能

1. 注销登录的前端实现

<a href="javascript:logout()">注销</a>
<script>
    function logout(){
        if(confirm("确认注销登录?")){
            jQuery.ajax({
                url:"/user/logout",
                type:"POST",
                data:{},
                success:function(res){
                    if(res != null && res.code == 200){
                        location.href = "/login.html";
                    }else{
                        alert("抱歉:操作失败,请重试!"+res.msg);
                    }
                }
            });
        }
    }
</script>

2. 注销登录的后端实现

在UserController类中实现logout方法:

/**
 * 注销(退出登录)
 */
@RequestMapping("/logout")
public AjaxResult logout(HttpSession session) {
    session.removeAttribute(AppVariable.USER_SESSION_KEY);
    return AjaxResult.success(1);
}

3. 删除博客功能前端实现

在博客详情页(myblog_list.html)中实现下面功能:

需要注意的时不能传递用户id参数,登录用户信息靠后端从session中获取:

<a href="javascript:logout()">注销</a>
<script>
    // 删除文章
    function myDel(id){
        if(confirm("确实删除?")){
            // 删除文章
            jQuery.ajax({
                url:"art/del",
                type:"POST",
                data:{"id":id},
                success:function(result){
                    if(result!=null && result.code==200 && result.data==1){
                        alert("恭喜:删除成功!");
                        // 刷新当前页面
                        location.href = location.href;
                    }else{
                        alert("抱歉:删除失败,请重试!");
                    }
                }
            });
        }
    }
</script>

4. 删除博客功能后端实现

在ArticleMapper接口定义del抽象方法

int del(@Param("id") Integer id, @Param("uid") Integer uid);

在ArticleMapper.xml中书写sql语句

<delete id="del">
    delete from articleinfo where id=#{id} and uid=#{uid}
</delete>

service包中的ArticleService类实现del方法

public int del(Integer id, Integer uid) {
    return articleMapper.del(id, uid);
}

在controller包中的ArticleController类下实现 del方法

@RequestMapping("/del")
public AjaxResult del(HttpServletRequest request, Integer id) {
    if (id == null || id <= 0) {
        // 参数有误
        return AjaxResult.fail(-1, "参数异常");
    }
    Userinfo user = UserSessionUtils.getUser(request);
    if (user == null) {
        return AjaxResult.fail(-2, "用户未登录");
    }
    return AjaxResult.success(articleService.del(id, user.getId()));
}

7. 博客详情页

1.展示博客内容前端实现

<!-- 右侧内容详情 -->
<div class="container-right">
    <div class="blog-content">
        <!-- 博客标题 -->
        <h3 id="title"></h3>
        <!-- 博客时间 -->
        <div class="date">发布时间:<span id="updatetime"></span> &nbsp;&nbsp;
            阅读量:<span id="rcount"></span>
        </div>
        <!-- 博客正文 -->
        <div id="editorDiv">

        </div>
    </div>
</div>
<script>
    // 获取当前url参数的方法
    function getUrlValue(key){
        // ex:?id=1&v=2
        var params = location.search;
        if(params.length>1){
            // ex:id=1&v=2
            params = location.search.substring(1);
            var paramArr = params.split("&");
            for(var i=0;i<paramArr.length;i++){
                var kv = paramArr[i].split("=");
                if(kv[0]==key){
                    // 是我要查询的参数
                    return kv[1];
                }
            }
        }
        return "";
    }
    // 查询文章详情
    function getArtDetail(id){
        if(id==""){
            alert("非法参数!");
            return;
        }
        jQuery.ajax({
            url:"art/detail",
            type:"POST",
            data:{"id":id},
            success:function(result){
                if(result!=null && result.code==200){
                    jQuery("#title").html(result.data.title);
                    jQuery("#updatetime").html(result.data.updatetime);
                    jQuery("#rcount").html(result.data.rcount);
                    initEdit(result.data.content);
                }else{
                    alert("查询失败,请重试!");
                }
            }
        });
    }
    getArtDetail(getUrlValue("id"));
</script>

2.展示博客内容后端实现

在ArticleMapper接口定义getDetail抽象方法

Articleinfo getDetail(@Param("id") Integer id);

在ArticleMapper.xml中书写sql语句

<select id="getDetail" resultType="com.example.demo.entity.Articleinfo">
    select * from articleinfo where id=#{id}
</select>

service包中的ArticleService类实现getDetail方法

public Articleinfo getDetail(Integer id) {
    return articleMapper.getDetail(id);
}

在controller包中的ArticleController类下实现 getDetail方法

@RequestMapping("/detail")
public AjaxResult getDetail(Integer id) {
    if (id == null || id <= 0) {
        return AjaxResult.fail(-1, "非法参数");
    }
    return AjaxResult.success(articleService.getDetail(id));
}

3.详情页作者信息展示前端

<!-- 左侧个人信息 -->
<div class="container-left">
    <div class="card">
        <img src="img/avatar.png" class="avtar" alt="">
        <h3 id="username"></h3>
        <a href="http:www.github.com">github 地址</a>
        <div class="counter">
            <span>文章</span>
            <span>分类</span>
        </div>
        <div class="counter">
            <span id="artCount"></span>
            <span>1</span>
        </div>
    </div>
</div>
<script>
    //此方法在getArtDetail方法中查询成功后调用showUser(result.data.uid);
    // 查询用户的详情信息
    function showUser(id){
        //注意后端往前端传递数据的时候,不要传递敏感信息,比如:个人密码
        jQuery.ajax({
            url:"/user/getuserbyid",
            type:"POST",
            data:{"id":id},
            success:function(result){
                if(result!=null && result.code==200 && result.data.id>0){
                    jQuery("#username").text(result.data.username);
                    jQuery("#artCount").text(result.data.artCount);
                }else{
                    alert("抱歉:查询用户信息失败,请重试!");
                }
            }
        });
    }
</script>

4.详情页作者信息展示后端

在ArticleMapper接口定义getUserById抽象方法

Userinfo getUserById(@Param("id") Integer id);

在ArticleMapper.xml中书写sql语句

<select id="getUserById" resultType="com.example.demo.entity.Userinfo">
    select * from userinfo where id=#{id}
</select>

service包中的ArticleService类实现getUserById方法

public Userinfo getUserById(Integer id) {
    return userMapper.getUserById(id);
}

在controller包中的ArticleController类下实现 getUserById方法

@RequestMapping("/getuserbyid")
public AjaxResult getUserById(Integer id) {
    if (id == null || id <= 0) {
        // 无效参数
        return AjaxResult.fail(-1, "非法参数");
    }
    Userinfo userinfo = userService.getUserById(id);
    if (userinfo == null || userinfo.getId() <= 0) {
        // 无效参数
        return AjaxResult.fail(-1, "非法参数");
    }
    // 去除 userinfo 中的敏感数据,ex:密码
    userinfo.setPassword("");
    UserinfoVO userinfoVO = new UserinfoVO();
    BeanUtils.copyProperties(userinfo, userinfoVO);
    // 查询当前用户发表的文章数
    userinfoVO.setArtCount(articleService.getArtCountByUid(id));
    return AjaxResult.success(userinfoVO);
}

5. 实现增加阅读量前端

<script>
    // 阅读量 +1
    function updataRCount(){
        // 先得到文章 id
        var id = getUrlValue("id");
        if(id!=""){
            jQuery.ajax({
                url:"/art/incr-rcount",
                type:"POST",
                data:{"id":id},
                success:function(result){}
            });
        }
    }
    updataRCount();
</script>

6.实现增加阅读量后端

在ArticleMapper接口定义incrRCount抽象方法

public int incrRCount(@Param("id") Integer id);

在ArticleMapper.xml中书写sql语句:

在实现sql语句的时候,不可以用先查询后修改的行为,因为这样是两个操作,不是原子性的,会出现线程安全问题。所以我们应给只进行一个修改操作就可以了。

<update id="incrRCount">
    update articleinfo set rcount=rcount+1 where id=#{id}
</update>

service包中的ArticleService类实现incrRCount方法

public int incrRCount(Integer id) {
    return articleMapper.incrRCount(id);
}

在controller包中的ArticleController类下实现 incrRCount方法

@RequestMapping("/incr-rcount")
public AjaxResult incrRCount(Integer id) {
    if (id != null && id > 0) {
        return AjaxResult.success(articleService.incrRCount(id));
    }
    return AjaxResult.fail(-1, "未知错误");
}

8. 新增文章页

前端实现

<!-- 编辑框容器 -->
<div class="blog-edit-container">
    <!-- 标题编辑区 -->
    <div class="title">
        <input type="text" id="title" placeholder="在这里写下文章标题">
        <button onclick="mysub()">发布文章</button>
    </div>
    <!-- 创建编辑器标签 -->
    <div id="editorDiv">
        <textarea id="editor-markdown" style="display:none;"></textarea>
    </div>
</div>
<script>
    // 提交
    function mysub(){
        if(confirm("确认提交?")){
            // 1.非空效验
            var title = jQuery("#title");
            if(title.val()==""){
                alert("请先输入标题!");
                title.focus();
                return;
            }
            if(editor.getValue()==""){
                alert("请先输入文章内容!");
                return;
            }
            // 2.请求后端进行博客添加操作
            jQuery.ajax({
                url:"/art/add",
                type:"POST",
                data:{"title":title.val(),"content":editor.getValue()},
                success:function(result){
                    if(result!=null && result.code==200 && result.data==1){
                        if(confirm("恭喜:文章添加成功!是否继续添加文章?")){
                            // 刷新当前页面
                            location.href = location.href;
                        }else{
                            location.href = "/myblog_list.html";
                        }
                    }else{
                        alert("抱歉,文章添加失败,请重试!");
                    }
                }
            });
        }
    }
</script>

后端实现

在ArticleMapper接口定义add抽象方法

int add(Articleinfo articleinfo);

在ArticleMapper.xml中书写sql语句:

在实现sql语句的时候,不可以用先查询后修改的行为,因为这样是两个操作,不是原子性的,会出现线程安全问题。所以我们应给只进行一个修改操作就可以了。

<insert id="add">
    insert into articleinfo(title,content,uid,updatetime) values(#{title},#{content},#{uid},#{updatetime})
</insert>

service包中的ArticleService类实现add方法

public int add(Articleinfo articleinfo) {
    return articleMapper.add(articleinfo);
}

在controller包中的ArticleController类下实现 add方法

@RequestMapping("/add")
public AjaxResult add(HttpServletRequest request, Articleinfo articleinfo) {
    // 1.非空效验
    if (articleinfo == null || !StringUtils.hasLength(articleinfo.getTitle()) ||
            !StringUtils.hasLength(articleinfo.getContent())) {
        // 非法参数
        return AjaxResult.fail(-1, "非法参数");
    }
    // 2.数据库添加操作
    // a.得到当前登录用户的 uid
    Userinfo userinfo = UserSessionUtils.getUser(request);
    if (userinfo == null || userinfo.getId() <= 0) {
        // 无效的登录用户
        return AjaxResult.fail(-2, "无效的登录用户");
    }
    articleinfo.setUid(userinfo.getId());
    articleinfo.setUpdatetime(LocalDateTime.now());//设置更新时间
    // b.添加数据库并返回结果
    return AjaxResult.success(articleService.add(articleinfo));
}

9. 博客修改页

前端实现

博客编辑页的初始化功能复用之前的博客详情页写的接口即可实现。需要新增的后端接口就是修改操作。

<!-- 编辑框容器 -->
<div class="blog-edit-container">
    <!-- 标题编辑区 -->
    <div class="title">
        <input id="title" type="text">
        <button onclick="mysub()">修改文章</button>
    </div>
    <!-- 创建编辑器标签 -->
    <div id="editorDiv">
        <textarea id="editor-markdown" style="display:none;"></textarea>
    </div>
</div>
<script>
    // 提交
    function mysub(){
        // 1.非空效验
        var title = jQuery("#title");
        if(title.val()==""){
            alert("请先输入标题!");
            title.focus();
            return;
        }
        if(editor.getValue()==""){
            alert("请先输入正文!");
            return;
        }
        // 2.进行修改操作
        jQuery.ajax({
            url:"/art/update",
            type:"POST",
            data:{"id":id,"title":title.val(),"content":editor.getValue()},
            success:function(result){
                if(result!=null && result.code==200 && result.data==1){
                    alert("恭喜:修改成功!");
                    location.href = "myblog_list.html";
                }else{
                    alert("抱歉:操作失败,请重试!");
                }
            }
        });
    }
    // 文章初始化
    function initArt(){
        // 得到当前页面 url 中的参数 id(文章id)
        id = getUrlValue("id");
        if(id==""){
            alert("无效参数");
            location.href = "myblog_list.html";
            return;
        }
        // 请求后端,查询文章的详情信息
        jQuery.ajax({
            url:"art/detail",
            type:"POST",
            data:{"id":id},
            success:function(result){
                if(result!=null && result.code==200){
                    jQuery("#title").val(result.data.title);
                    initEdit(result.data.content);
                }else{
                    alert("查询失败,请重试!");
                }
            }
        });
    }
    initArt();
</script>

后端实现

在ArticleMapper接口定义update抽象方法

int update(Articleinfo articleinfo);

在ArticleMapper.xml中书写sql语句:

在实现sql语句的时候,不可以用先查询后修改的行为,因为这样是两个操作,不是原子性的,会出现线程安全问题。所以我们应给只进行一个修改操作就可以了。

<update id="update">
    update articleinfo set title=#{title},content=#{content},updatetime=#{updatetime}
    where id=#{id} and uid=#{uid}
</update>

service包中的ArticleService类实现update方法

public int update(Articleinfo articleinfo) {
    return articleMapper.update(articleinfo);
}

在controller包中的ArticleController类下实现 update方法

@RequestMapping("/update")
public AjaxResult update(HttpServletRequest request, Articleinfo articleinfo) {
    // 非空效验
    if (articleinfo == null || !StringUtils.hasLength(articleinfo.getTitle()) ||
            !StringUtils.hasLength(articleinfo.getContent()) ||
            articleinfo.getId() == null) {
        // 非法参数
        return AjaxResult.fail(-1, "非法参数");
    }
    // 得到当前登录用户的 id
    Userinfo userinfo = UserSessionUtils.getUser(request);
    if (userinfo == null && userinfo.getId() == null) {
        // 无效用户
        return AjaxResult.fail(-2, "无效用户");
    }
    // 很核心的代码(解决了修改文章归属人判定的问题)
    articleinfo.setUid(userinfo.getId());
    articleinfo.setUpdatetime(LocalDateTime.now());
    return AjaxResult.success(articleService.update(articleinfo));
}

10. 密码MD5加盐加密

1.自实现加密的工具类

加盐加密的实现思路:

  1. 加密的思路:每次调用方法时产生唯一的盐值 + 密码 = 最终密码(MD5加密)
  2. 解密思路:需要两个密码:需要验证的密码 + 最终加密的密码
    • 核心思想:得到盐值。将盐值存放到最终密码的某个为止,然后在最终密码中得到盐值
    • 通过: 需要验证的密码+盐值,走一遍加密同样的过程得到的密码和最终密码比较

自己定义一个工具类,其中定义如何进行加密和解密的方法

public class PasswordUtils {
    /**
     * 1.加盐并生成密码
     * @param password 明文密码
     * @return 保存到数据库中的密码
     */
    public static String encrypt(String password) {
        // 1.产生盐值(32位)
        String salt = UUID.randomUUID().toString().replace("-", "");
        // 2.生成加盐之后的密码
        String saltPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
        // 3.生成最终密码(保存到数据库中的密码)【约定格式:32位盐值+$+32位加盐之后的密码】
        String finalPassword = salt + "$" + saltPassword;
        return finalPassword;
    }

    /**
     * 2.生成加盐的密码(方法1的重载)
     * @param password 明文
     * @param salt     固定的盐值
     * @return 最终密码
     */
    public static String encrypt(String password, String salt) {
        // 1.生成一个加盐之后的密码
        String saltPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
        // 2.生成最终的密码【约定格式:32位盐值+$+32位加盐之后的密码】
        String finalPassword = salt + "$" + saltPassword;
        return finalPassword;
    }

    /**
     * 3.验证密码
     * @param inputPassword 用户输入的明文密码
     * @param finalPassword 数据库保存的最终密码
     * @return
     */
    public static boolean check(String inputPassword, String finalPassword) {
        if (StringUtils.hasLength(inputPassword) && StringUtils.hasLength(finalPassword) &&
                finalPassword.length() == 65) {
            // 1.得到盐值
            String salt = finalPassword.split("\\$")[0];
            // 2.使用之前加密的步骤,将明文密码和已经得到的盐值进行加密,生成最终的密码
            String confirmPassword = PasswordUtils.encrypt(inputPassword, salt);
            // 3.对比两个最终密码是否相同
            return confirmPassword.equals(finalPassword);
        }
        return false;
    }
}

2. 在注册和登录页面修改加密

主要是对UserController类中的reg方法和login方法进行修改

@RequestMapping("/reg")
public AjaxResult reg(Userinfo userinfo) {
    // 非空效验和参数有效性效验
    if (userinfo == null || !StringUtils.hasLength(userinfo.getUsername()) ||
            !StringUtils.hasLength(userinfo.getPassword())) {
        return AjaxResult.fail(-1, "非法参数");
    }
    // 密码加盐处理
    userinfo.setPassword(PasswordUtils.encrypt(userinfo.getPassword()));
    userinfo.setUpdatetime(LocalDateTime.now());//设置更新时间
    return AjaxResult.success(userService.reg(userinfo));
}
@RequestMapping("/login")
public AjaxResult login(HttpServletRequest request,String username,String password){
    // 非空效验和参数有效性效验
    if (username == null || !StringUtils.hasLength(username) ||
            !StringUtils.hasLength(password) ){
        return AjaxResult.fail(-1, "非法参数");
    }
    // 2.查询数据库
    Userinfo userinfo = userService.getUserByName(username);
    if (userinfo != null && userinfo.getId() > 0) {   // 有效的用户
        // 两个密码是否相同
        if (PasswordUtils.check(password, userinfo.getPassword())) {
            // 登录成功
            // 将用户存储到 session
            HttpSession session = request.getSession();
            session.setAttribute(AppVariable.USER_SESSION_KEY, userinfo);
            userinfo.setPassword(""); // 返回前端之前,隐藏敏感(密码)信息
            return AjaxResult.success(userinfo);
        }
    }
    return AjaxResult.success(0,null);
}

3. 使用spring security进行加盐加密

  1. 引入spring security框架

1698306874541

spring security有自己的认证体系,我们对其进行访问的时候,必须先登录spring security的登录界面

image-20231026155711227

产生这个的原因是:这个加入框架之后,是会产生spring boot的自动注入的。

  1. 排除spring security自动加载

在启动类上加:@SpringBootApplication(exclude = SecurityAutoConfiguration.class)

@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
public class MycnblogApplication {
    public static void main(String[] args) {
        SpringApplication.run(MycnblogApplication.class, args);
    }
}
  1. 调用 实现加盐和验证
    @Test
    void testSecurity(){
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String password = "123456";
        String finalPassword = passwordEncoder.encode(password);
        System.out.println(finalPassword);
        System.out.println(passwordEncoder.encode(password));
        System.out.println(passwordEncoder.encode(password));
        //解密:参数1:明文密码。参数2:最终密码
        String inputPassword = "12345";
        System.out.println("错误密码比对结果:" + passwordEncoder.matches(inputPassword,finalPassword));
        String inputPassword2 = "123456";
        System.out.println("错误密码比对结果:" + passwordEncoder.matches(inputPassword2,finalPassword));
    }

在测试类中实现上面的测试方法。发现每次加盐的结果都是不同的。

11. 分页展示实现

1.分页功能的后端

分页公式的值 = (当前页码 - 1) * 每页显示的条数

在ArticleMapper接口定义getListByPage和getCount抽象方法

List<Articleinfo> getListByPage(@Param("psize") Integer psize,
                                @Param("offsize") Integer offsize);
int getCount();

在ArticleMapper.xml中书写sql语句:

在实现sql语句的时候,不可以用先查询后修改的行为,因为这样是两个操作,不是原子性的,会出现线程安全问题。所以我们应给只进行一个修改操作就可以了。

<select id="getListByPage" resultType="com.example.demo.entity.Articleinfo">
    select * from articleinfo limit #{psize} offset #{offsize}
</select>
<select id="getCount" resultType="Integer">
    select count(*) from articleinfo
</select>

service包中的ArticleService类实现getListByPage和getCount方法

public List<Articleinfo> getListByPage(Integer psize, Integer offsize) {
    return articleMapper.getListByPage(psize, offsize);
}
public int getCount() {
    return articleMapper.getCount();
}

在controller包中的ArticleController类下实现 getListByPage方法

@RequestMapping("/listbypage")
public AjaxResult getListByPage(Integer pindex, Integer psize) {
    // 1.参数校正
    if (pindex == null || pindex <= 1) {
        pindex = 1;
    }
    if (psize == null || psize <= 1) {
        psize = 2;
    }
    // 分页公式的值 = (当前页码-1)*每页显示条数
    int offset = (pindex - 1) * psize;
    // 文章列表数据
    List<Articleinfo> list = articleService.getListByPage(psize, offset);
    // 当前列表总共有多少页
    // a.总共有多少条数据
    int totalCount = articleService.getCount();
    // b.总条数/psize(每页显示条数)
    double pcountdb = totalCount / (psize * 1.0);
    // c.使用进一法得到总页数
    int pcount = (int) Math.ceil(pcountdb);
    HashMap<String, Object> result = new HashMap<>();
    result.put("list", list);
    result.put("pcount", pcount);
    return AjaxResult.success(result);
}

2. 分页功能的前端

版心部分的html代码:

<!-- 版心 -->
<div class="container">
    <!-- 右侧内容详情 -->
    <div class="container-right" style="width: 100%;">
        <div id="artListDiv">
        </div>

        <hr>
        <div class="blog-pagnation-wrapper">
            <button onclick="goFirstPage()" class="blog-pagnation-item">首页</button>
            <button onclick="goBeforePage()" class="blog-pagnation-item">上一页</button>
            <button onclick="goNextPage()" class="blog-pagnation-item">下一页</button>
            <button onclick="goLastPage()" class="blog-pagnation-item">末页</button>
        </div>
    </div>
</div>
js代码
// 当前页码
var pindex = 1;
// 每页显示条数
var psize = 2;
// 最大页数
var pcount =1;
// 1.先尝试得到当前 url 中的页码
pindex = (getUrlValue("pindex")==""?1:getUrlValue("pindex"));
// 2.查询后端接口得到当前页面的数据,进行展示
function initPage(){
   jQuery.ajax({
    url:"/art/listbypage",
    type:"POST",
    data:{"pindex":pindex,"psize":psize},
    success:function(result){
        if(result!=null && result.code==200 && result.data.list.length>0){
            var artListHtml = "";
            for(var i=0;i<result.data.list.length;i++){
                var articleinfo = result.data.list[i];
                artListHtml +='<div class="blog">';
                artListHtml +='<div class="title">'+articleinfo.title+'</div>';
                artListHtml +='<div class="date">'+articleinfo.updatetime+'</div>';
                artListHtml +='<div class="desc">'+articleinfo.content+'</div>';
                artListHtml +='<a href="blog_content.html?id='+ articleinfo.id
                    +'" class="detail">查看全文 &gt;&gt;</a>';
                artListHtml +='</div>';    
            }
            jQuery("#artListDiv").html(artListHtml);
            pcount = result.data.pcount;
        }
    }
   });     
}
initPage();
// 跳转到首页
function goFirstPage(){
    if(pindex<=1){
        alert("当前已经在首页了");
        return;
    }
    location.href = "blog_list.html";
}
// 点击上一页按钮
function goBeforePage(){
    if(pindex<=1){
        alert("当前已经在首页了");
        return;
    }
    pindex = parseInt(pindex) -1;
    location.href ="blog_list.html?pindex="+pindex;
}
function goNextPage(){
    if(pindex>=pcount){
       alert("已经在末页了");
       return; 
    }
    pindex = parseInt(pindex)+1;
    location.href ="blog_list.html?pindex="+pindex;
}
function goLastPage(){
    if(pindex>=pcount){
       alert("已经在末页了");
       return; 
    }
    location.href ="blog_list.html?pindex="+pcount;
}

上述就实现了博客系统最基本的功能了

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值