ssm个⼈技术博客+MyBatisPlus

1.项⽬使⽤技术

  1.  Spring Boot(最新 Spring Boot 2.x) -> JDK 1.8+
  2. Spring Web(Spring MVC)
  3.  MyBatis
  4. MyBatis Plus
  5. MySQL(5.x)
  6. Redis(3.x)-> 存储验证码/存储⽤户登录信息

1.1 项⽬前端资源⽂件

JavaEE: spring的项目学习 - Gitee.com

1.2 项⽬源码

JavaEE: spring的项目学习 - Gitee.com

2.项⽬交互流程和⻚⾯展示

1、所有⼈实现进⼊到⽂章列表⻚,展示所有⼈发布的⽂章

2、点击详情查看⽂章正⽂
3、点击注册,在博客平台注册账号(之后就可以发布⽂章、管理⽂章、发评论等操作了)
登录和注册都需要每次⽣成不同的验证码,并且验证成功之后才能执⾏业务逻辑。
4、注册成功之后就可以登录了
5、登录完成,可以发表⽂章
6、发表完⽂章可以在我的⽂章列表中查看和管理
7、点击修改,到修改⻚⾯
8、删除操作
删除操作,需要验证归属⼈,需要提供防⽌误删的功能。
9、评论相关功能
10、个⼈中⼼

3.数据库分析和设计

1. ⽤户表

a. ⽤户主键(⽤户编号)
b. 登录⽤户名(需要添加唯⼀约束)
c. 昵称
d. 密码
e. 个⼈ github 地址
f. 头像
g. 状态(隐式字段)【正常、异常、被永久冻结、被临时冻结】

2. ⽂章表

a. ⽂章主键
b. 标题
c. 创建时间
d. 修改时间
e. ⽂章简介
f. ⽂章正⽂
g. 状态(隐私字段)【已发布、草稿】
h. ⽤户主键(⽂章作者id)
i. 访问量

3. 评论表

a. 评论表主键
b. ⽂章编号
c. ⽤户主键(评论发表⼈是谁)
d. 评论的正⽂
e. 评论发表时间

SQL 脚本

1.创建⽤户表

-- 创建⽤户表
create table userinfo(
    uid bigint auto_increment primary key comment '主键',
    loginname varchar(50) not null unique comment '登录⽤户名',
    nickname varchar(50) default '' comment '昵称',
    `password` varchar(65) not null comment '密码',
    github varchar(255) comment 'github地址',
    photo varchar(255) comment '头像',
    `state` tinyint not null DEFAULT 1 comment '⽤户状态,1=正常|-1=异常|-2=永久冻结|-3=临时冻结'
);
insert into userinfo(loginname,`password`) values('admin','admin');
2.创建⽂章表
create table articleinfo(
    aid BIGINT auto_increment primary key comment '主键',
    `title` varchar(255) not null comment '标题',
    createtime TIMESTAMP not null default CURRENT_TIMESTAMP() comment '创建时间',
    updatetime TIMESTAMP not null DEFAULT CURRENT_TIMESTAMP() comment '修改时间',
    `desc` varchar(255) not null comment '⽂章简介',
    `content` longtext not null comment '⽂章正⽂',
    `state` TINYINT DEFAULT -1 comment '状态:-1=草稿|1=已发布',
    uid bigint not null comment '作者id',
    `rcount` bigint default 1 comment '阅读量'
);
3.创建评论表
-- 创建评论表
create table commentinfo(
    cid bigint auto_increment primary key comment '评论表的主键',
    aid bigint not null comment '⽂章表id',
    uid bigint not null comment '⽤户id',
    `content` varchar(500) not null comment '评论正⽂',
    createtime TIMESTAMP default CURRENT_TIMESTAMP() comment '评论的发表时间'
);

4.核⼼实现代码

4.1 项⽬常⽤配置

# 1.配置 MySQL 连接信息
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mycnblog?characterEncodi
ng=utf8
spring.datasource.username=root
# 注意:下⾯是你⾃⼰服务器的 MySQL 密码
spring.datasource.password=12345678
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 2.配置 MyBatis XML 保存⽬录
mybatis.mapper-locations=classpath:mybatis/*Mapper.xml
# 配置 MyBatis 打印 SQL ⽇志
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
logging.level.com.javacn.myblog=debug
# 配置 Redis 连接信息
spring.session.store-type=redis
spring.data.redis.host=127.0.0.1
spring.data.redis.password=
spring.data.redis.port=6379
server.servlet.session.timeout=1800
spring.session.redis.flush-mode=on_save
spring.session.redis.namespace=spring:session

4.2 MyBatis XML 模版

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybati
s.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="">
</mapper>

4.3 ⽤户权限效验拦截器实现代码

拦截器:
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    // 1.先得到 HttpSession 对象
    HttpSession session = request.getSession(false);
    if(session!=null && session.getAttribute(AppVar.SESSION_KEY_USERINFO)!=null){
    // 已经登录
        return true;
    }
    // 代码执⾏到此处,说明⽤户未登录
    response.sendRedirect("/login.html");
        return false;
    }
}
拦截规则:
@Configuration
public class MyConfig implements WebMvcConfigurer {
    @Resource
    private LoginInterceptor loginInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
        .addPathPatterns("/**") // 拦截所有请求
        .excludePathPatterns("/user/reg") // 排除注册接⼝(注册接⼝不拦截)
        .excludePathPatterns("/user/login")
        .excludePathPatterns("/user/getuser")
        .excludePathPatterns("/**/*.html")
        .excludePathPatterns("/css/**")
        .excludePathPatterns("/editor.md/**")
        .excludePathPatterns("/img/**")
        .excludePathPatterns("/js/**");
    }
}

4.4 密码加盐实现代码

public class PasswordUtil {
    public static String encrypt(String password) {

        String salt = UUID.randomUUID().toString().replace("-", "");
        String finalPassword = DigestUtils.md5DigestAsHex((salt+password).getBytes());
        String dbpassword = salt+"$"+finalPassword;
        return dbpassword;
    }

    public static boolean decrypt(String inputPassword, String dbPassword) {
        if (!StringUtils.hasLength(inputPassword) || !StringUtils.hasLength(dbPassword) ||
            dbPassword.length() != 65 || !dbPassword.contains("$")) {
            return false;
        }
        String[] dbPasswordArray = dbPassword.split("\\$");
        String salt = dbPasswordArray[0];
        String finalPassword = dbPasswordArray[1];
        String userPassword = DigestUtils.md5DigestAsHex((salt+inputPassword).getBytes());
        if (userPassword.equals(finalPassword)) {
            return true;
        }
        return false;
    }
}

4.5 验证码

⽣成验证码:
@RestController
public class CaptchaController {
     @Value("${imagepath}")
     private String imagepath;
     @Autowired
     private RedisTemplate redisTemplate;
     @RequestMapping("/getcaptcha")
     public AjaxResult getCaptcha(){
         // 1.⽣成验证码到本地
         //定义图形验证码的⻓和宽
         LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(128, 50);
         String uuid = UUID.randomUUID().toString().replace("-","");
         // 图形验证码写出,可以写出到⽂件,也可以写出到流
         lineCaptcha.write(imagepath+uuid+".png");
         // url 地址
         String url = "/image/"+uuid+".png";
         // 将验证码存储到 redis
         redisTemplate.opsForValue().set(uuid,lineCaptcha.getCode());
         HashMap<String,String> result = new HashMap<>();
         result.put("codeurl",url);
         result.put("codekey",uuid);
         return AjaxResult.succ(result);
     }
}
配置本地图⽚的拦截映射规则:
@Value("${imgpath}")
private String imagePath;
/**
 * 映射图⽚路径
 * @param registry
 */
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
     registry.addResourceHandler("/image/**").addResourceLocations("file:"+ imagePath);
}

5.准备项目中的公共模块

1. 实体层 (model) - 实体类
2. 控制器层 (controller) -> 控制器
3. 服务层 (service) -> 服务类
4. 持久层 (mapper) -> mapper
5. 工具层 (common) -> 统一返回类

5.1 实体层 (model) - 实体类

@Setter
@Getter
@TableName("userinfo")
public class UserInfo implements Serializable {
    private static final long serialVersionUID = 4535455246930079692L;
    @TableId(type= IdType.AUTO)
    private long uid;
    private String loginname;
    private String nickname;
    private String password;
    private String github;
    private String photo;
    private int state;
}
@Setter
@Getter
@TableName("articleinfo")
public class ArticleInfo implements Serializable {
    private static final long serialVersionUID = 3283853718086621555L;
    @TableId(type= IdType.AUTO)
    private long aid;
    private String title;
    private LocalDateTime createtime;
    private LocalDateTime updatetime;
    @TableField("`desc`")
    private String desc;
    private String content;
    private int state;
    private long uid;
    private long rcount;
}
@Setter
@Getter
@TableName("commentinfo")
public class CommentInfo implements Serializable {
    private static final long serialVersionUID = -1047755250188117438L;
    @TableId(type= IdType.AUTO)
    private long cid;
    private long aid;
    private long uid;
    private String content;
    private LocalDateTime createtime;
}

创建vo包,放置扩展类

@Setter
@Getter
public class ArticleInfoVO extends ArticleInfo implements Serializable {
    private static final long serialVersionUID = 8224133111164008018L;
    private String photo;
    private String nickname;
    private int artCount;
}
@Setter
@Getter
public class CommentInfoVO extends CommentInfo implements Serializable {
    private static final long serialVersionUID = 6853223848170457870L;
    private String nickname;
}
@Setter
@Getter
public class UserInfoVO extends UserInfo implements Serializable {
    private static final long serialVersionUID = 1814588586869124290L;
    private String checkCode;
    private String codeKey;
}

5.2 控制器层 (controller) -> 控制器

@RestController
@RequestMapping("/art")
public class ArticleController {

    private static final Integer PAGE_PAGE_SIZE = 120;
    @Autowired
    private IArticleService articleService;

    private static final int _MAX_DESC_LENGTH = 120;
}
@RestController
public class CaptchaController {

    @Value("${imagepath}")
    private String imagepath;

    @Autowired
    private RedisTemplate redisTemplate;
}
@RestController
@RequestMapping("/comment")
public class CommentController {

    @Resource
    private ICommentService commentService;

    @Resource
    private IArticleService articleService;
}
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private IUserService userService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private IArticleService articleService;

    @Value("${imagepath}")
    private String imagepath;
}

5.3 服务层 (service) -> 服务类

public interface IArticleService extends IService<ArticleInfo> {
}
public interface ICommentService extends IService<CommentInfo> {
}
public interface IUserService extends IService<UserInfo> {
}

创建impl包,并创建以下类:

@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, ArticleInfo> implements IArticleService {
    @Resource
    private ArticleMapper articleMapper;

}
@Service
public class CommentServiceImpl extends ServiceImpl<CommentMapper, CommentInfo> implements ICommentService {
    @Resource
    private CommentMapper commentMapper;

}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, UserInfo> implements IUserService {
}

5.4 持久层 (mapper) -> mapper

@Mapper
public interface ArticleMapper extends BaseMapper<ArticleInfo> {
}
@Mapper
public interface CommentMapper extends BaseMapper<CommentInfo> {
}
@Mapper
public interface UserMapper extends BaseMapper<UserInfo> {
}

5.5 工具层 (util) -> 统一返回类

统一数据的返回

对返回的数据进行分类 :

  • 成功返回

  • 失败

1. 定义一个数据返回类 - AjaxResult.java

@Setter
@Getter
@ToString
public class AjaxResult implements Serializable {
    private static final long serialVersionUID = 6576894573990501938L;
    //状态码
    private int code;
    //描述信息
    private String msg;
    //返回数据
    private Object data;

    /**
     * 返回统一成功时的方法
     * @param data
     * @return
     */
    public static AjaxResult succ(Object data) {
        AjaxResult result = new AjaxResult();
        result.setCode(200);
        result.setMsg("");
        result.setData(data);
        return result;
    }

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

    /**
     * 返回统一失败对象
     * @param code
     * @param msg
     * @return
     */
    public static AjaxResult fail(int code, String msg) {
        AjaxResult result = new AjaxResult();
        result.setCode(code);
        result.setMsg(msg);
        result.setData("");
        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;
    }
}

统一 session 的验证

这个功能可以不用统一处理, 只不过多个地方都要进行验证, 为了减少多处写相同的代码, 所以进行了封装.

/**
 * 登录用户的工具类
 */
public class SessionUtil {
    public static UserInfo getUserInfo(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if(session != null && session.getAttribute(AppVar.SESSION_KEY_USERINFO) != null) {
            return (UserInfo) session.getAttribute(AppVar.SESSION_KEY_USERINFO);
        }
        return null;
    }
}

统一常量类

此处的常量类主要是用户登录成功后, 将其身份信息存储 session 时, 需要多次写字符串常量, 为了避免写错, 所以专门定义一个常量类, 就只需要写一次即可.

/**
 * 全局公共变量
 */
public class AppVar {
    public static final String SESSION_KEY_USERINFO = "SESSION_KEY_USERINFO";
}

6.实现相关功能    

6.1 实现登录注册功能  

6.1.1 实现登录注册功能  

第一步: 引入用于发送 ajax 请求的依赖

 <script src="js/jquery.min.js"></script>

第二步 :登录的请求和响应设计

请求:
{
    post,
    /user/reg
    data:{codeKey,loginname,password,checkcode}
}
响应:
{
    "status": 200,
    "message": "注册成功",
    "data": {
        1
    }
}
响应体设计字段解释:
{
    状态码,为200代表成功,负数代表失败
    状态描述信息,描述此次请求成功或者失败的原因
    返回的数据,请求成功后,需要给前端的数据信息
}

第三步 :点击事件触发的 ajax 请求

验证码ajax请求

// 验证码key
    var codeKey = "";

    // 获取并显示验证码
    function loadCode() {
        jQuery.ajax({
            url: "/getcaptcha",
            type: "GET",
            data: {},
            success: function (res) {
                if (res.code = 200 && res.data != null && res.data != "") {
                    // 获取验证码成功
                    codeKey = res.data.codekey;
                    jQuery("#codeimg").attr("src", res.data.codeurl);
                }
            }
        });
    }

注册功能请求

// 请求后端实现注册功能
    function doReg() {
        // 1.进行非空效验
        var username = jQuery("#username"); // 等于原生 document.getElementById("username")
        var password = jQuery("#password");
        var password2 = jQuery("#password2");
        var checkcode = jQuery("#checkcode");
        if (username.val().trim() == "") {
            alert("请先输入用户名!");
            username.focus();
            return false;
        }
        if (password.val().trim() == "") {
            alert("请先输入密码!");
            password.focus();
            return false;
        }
        if (password2.val().trim() == "") {
            alert("请先输入确认密码!");
            password2.focus();
            return false;
        }
        if (checkcode.val().trim() == "") {
            alert("请先输入验证码!");
            checkcode.focus();
            return false;
        }
        // 密码强度效验(密码的长度必须大于 8)【扩展,密码组合强度效验】
        if(password.val().length<8){
            alert("抱歉:密码强度太低,密码位数必须大于等于8位!");
            password.focus();
            return false;
        }

        // 2.效验密码和确认密码要一致
        if (password.val() != password2.val()) {
            alert("两次输入的密码不一致,请先检查!");
            password.focus();
            return false;
        }
        // 3.发送 ajax 请求
        jQuery.ajax({
            url: "/user/reg",
            type: "POST",
            data: {
                "codeKey": codeKey,
                "loginname": username.val(),
                "password": password.val(),
                "checkCode": checkcode.val()
            }, success: function (res) {
                if(res.code == -2) {

                    username.focus();
                }
                // 接受返回结果
                // 4.根据返回的结果,将结果呈现给用户
                if (res.code == 200 && res.data == 1) {
                    // 添加成功
                    alert("恭喜:注册成功!");
                    location.href = "login.html"; // 跳转到登录页面
                } else {
                    alert("抱歉:操作失败!" + res.msg);
                }
            }
        });
    }

第四步: 写后端代码

注册功能涉及到的数据库操作就是新增功能.

写 controller 层代码 (UserController)

    @RequestMapping("/reg")
    public AjaxResult reg(UserInfoVO userInfoVO) {
        //1。校验参数的正确性
        if(userInfoVO == null || !StringUtils.hasLength(userInfoVO.getLoginname()) ||
                !StringUtils.hasLength(userInfoVO.getPassword()) ||
                !StringUtils.hasLength(userInfoVO.getCheckCode()) ||
                !StringUtils.hasLength(userInfoVO.getCodeKey())) {
            return AjaxResult.fail(-1, "非法参数");
        }
        String redisCodeValue = (String)      redisTemplate.opsForValue().get(userInfoVO.getCodeKey());
        if (!StringUtils.hasLength(redisCodeValue) || !redisCodeValue.equals(userInfoVO.getCheckCode())) {
            return AjaxResult.fail(-1, "验证码错误");
        }

        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("loginname", userInfoVO.getLoginname());
        UserInfo user = userService.getOne(queryWrapper);
        if(user != null) {
            return AjaxResult.fail(-2,"用户名已存在");
        }
        userInfoVO.setPassword(PasswordUtil.encrypt(userInfoVO.getPassword()));
        //2.调用服务器执行添加条件(添加到数据库)
        boolean result = userService.save(userInfoVO);
        //3。将执行结果返回给前端
        return AjaxResult.succ(result?1:0);
    }

此处先进行明文存储, 方便后续操作; 等全部功能基本实现时, 再将密码升级为加盐存储.

另外注册页面导航栏的右上角只有登录和主页两个按钮:

  • 登录 : 有些用户已经有账号了,, 就可以直接跳转到登录页面;
  • 主页 : 像掘金和 CSDN 这种网站吗没有登录也是可以访问主页别人写的博客的.
 

6.1.2 实现登录功能

登录的请求和响应设计

请求:
{
    get,
    /user/login
    data:{codeKey,username,password,checkcode}
}
响应:
{
    "status": 200,
    "message": "登录成功",
    "data": {
        "uid": xxxxx,
        "loginname": xxxxxx,
        "password": xxxxxxxx
        "nickname": xxxxxxxx
        "github": xxxxxxxxxx
        "photo": xxxxxxxxxxx
        "state": xxxxxxxxxxx
    }
}
响应体设计字段解释:
{
    状态码,为200代表成功,负数代表失败
    状态描述信息,描述此次请求成功或者失败的原因
    返回的数据,请求成功后,需要给前端的数据信息
}

ajax 请求

    function mySub() {
        // 1.非空效验
        var loginname = jQuery("#username");
        var password = jQuery("#password");
        var checkCode = jQuery("#checkCode");
        if (loginname.val().trim() == "") {
            alert("请先输入用户名!");
            loginname.focus();
            return false;
        }
        if (password.val().trim() == "") {
            alert("请先输入密码!");
            password.focus();
            return false;
        }
        if (checkCode.val().trim() == "") {
            alert("请先输入验证码!");
            checkCode.focus();
            return false;
        }
        // 2.发送 ajax 请求到后端
        jQuery.ajax({
            url: "/user/login",
            type: "GET",
            data: {
                "codeKey": codeKey,
                "loginname": loginname.val(),
                "password": password.val(),
                "checkCode": checkCode.val()
            }, success: function (res) {
                // 3.根据后端返回的结果,展示用户
                if (res.code == 200 && res.data != null && res.data.uid > 0) {
                    // 登录成功
                    location.href = "blog_list.html"; // 跳转到我的文章管理页
                } else {
                    alert("抱歉:登录失败!" + res.msg);
                }
            }
        });
    }

controller 层代码 (UserController)

    @RequestMapping("/login")
    public AjaxResult login(UserInfoVO userInfoVO, HttpSession session) {
        if(userInfoVO == null || !StringUtils.hasLength(userInfoVO.getLoginname()) ||
            !StringUtils.hasLength(userInfoVO.getCheckCode()) ||
            !StringUtils.hasLength(userInfoVO.getCodeKey())) {
            return AjaxResult.fail(-1, "非法参数!");
        }
        String redisCodeValue = (String) redisTemplate.opsForValue().get(userInfoVO.getCodeKey());
        if (!StringUtils.hasLength(redisCodeValue) || !redisCodeValue.equals(userInfoVO.getCheckCode())) {
            return AjaxResult.fail(-1, "验证码错误");
        }
        redisTemplate.opsForValue().set(userInfoVO.getCodeKey(), "");
        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("loginname", userInfoVO.getLoginname());
        UserInfo userInfo = userMapper.selectOne(queryWrapper);
        if(userInfo == null || userInfo.getUid() <= 0) {
            return AjaxResult.fail(-1, "用户名或密码错误!");
        }

        if(PasswordUtil.decrypt(userInfoVO.getPassword(), userInfo.getPassword())) {

            session.setAttribute(AppVar.SESSION_KEY_USERINFO, userInfo);
            userInfo.setPassword("");
            return AjaxResult.succ(userInfo);
        }else {
            return AjaxResult.fail(-1,"用户名或密码错误!");
        }
    }

6.2 实现退出登录功能

该页面也是需要用户登录效验的, 只有登录了, 才会有退出登录.

退出的请求和响应设计

请求:
{
    post,
    /user/logout
    data:{}
}
响应:
{
    "status": 200,
    "message": "退出成功",
    "data": {
        1
    }
}

ajax请求

    function logout() {
        // 1.先让用户确认是否真的退出系统
        if (confirm("是否确认退出?")) {
            // 2.如果点击的是“确认”,发送 ajax 请求给后端(执行注销操作)
            jQuery.ajax({
                url: "/user/logout",
                type: "POST",
                data: {},
                success: function (res) {
                    // 3.最后,再将后端返回的结果呈现给用户
                    if (res.code == 200 && res.data == 1) {
                        // 退出成功
                        alert("退出成功!");
                        location.href = "/login.html"; // 跳转到登录页
                    } else {
                        alert("抱歉:操作失败,请重试!" + res.msg);
                    }
                }
            });
        }
    }

controller 层代码 (UserController)

    @RequestMapping("/logout")
    public AjaxResult logout(HttpSession session) {
        try {
            session.removeAttribute(AppVar.SESSION_KEY_USERINFO);
        }catch (Exception e) {
            return AjaxResult.fail(-1,"清除失败");
        }

        return AjaxResult.succ(1);
    }

6.3 实现登录人的博客列表页

该页面是需要进行用户登录效验的, 所以该功能的后端接口需要被拦截器拦截.

该页面要实现的功能:
1. 展现登录人写的所有博客, 如果此人没写博客, 就显示暂无数据.
2. 在左侧展示登录人的个人信息, 此处要动态展示的个人信息有用户名(username)和文章数量

博客列表的请求和响应设计

请求:
{
    get,
    /art/mylist
    data:{}
}
响应:
{
    "status": 200,
    "message": "",
    "data": [
        {
            "aid": *,
            "title": *****,
            "createtime": ******************,
            "updatetime": ******************,
            "desc": ***************************************,
            "content": *************************************,
            "state": *,
            "uid": *,
            "rcount": **
        }
    ]
}

ajax请求

    // 查询我的所有文章
    function initPage() {
        jQuery.ajax({
            url: "/art/mylist",
            type: "GET",
            data: {},
            success: function (res) {
                var createHtml = "";
                if (res.code == 200 && res.data != null && res.data.length > 0) {
                    // 我已经发表文章了
                    let art = res.data[0];
                    initArtDetail(art.aid);
                    for (let i = 0; i < res.data.length; i++) {
                        let art = res.data[i];
                        createHtml += '<div class="blog">';
                        createHtml += '<div class="title">' + art.title + '</div>';
                        createHtml += '<div class="date">' + art.createtime + '</div>';
                        createHtml += '<div class="desc">';
                        createHtml += art.desc;
                        createHtml += '</div>';
                        createHtml += '<a href="blog_content.html?aid=' +
                            art.aid + '" class="detail">查看全文 &gt;&gt;</a>&nbsp;&nbsp;';
                        createHtml += '<a href="blog_edit.html?aid=' +
                            art.aid + '" class="detail">修改 &gt;&gt;</a>&nbsp;&nbsp;';
                        createHtml += '<a href="javascript:del(' +
                            art.aid + ')" class="detail">删除 &gt;&gt;</a>';
                        createHtml += '</div>';
                    }

                } else {
                    createHtml = "<h2 style='margin-left: 50px;margin-top: 20px;'>暂无数据,请先添加!</h2>";
                }
                jQuery("#artlist").html(createHtml);
            }
        });
    }

controller 层代码 (ArticleController)

    @RequestMapping("/mylist")
    public AjaxResult myList(HttpServletRequest request) {
        UserInfo userInfo = SessionUtil.getUserInfo(request);
        if (userInfo == null || userInfo.getUid() <= 0) {
            return AjaxResult.fail(-1,"当前用户未登录");
        }
        QueryWrapper<ArticleInfo> wrapper = new QueryWrapper<>();
        wrapper.orderByDesc(true, "aid");
        wrapper.eq("uid", userInfo.getUid());
        List<ArticleInfo> list = articleService.list(wrapper);
        return AjaxResult.succ(list);
    }

 6.4 实现博客详情页

此处的博客详情页, 是不能被拦截器拦截的, 因为主页有一个没有登录就可以访问的所有人的分页列表页, 在用户没有登录的情况下也是可以查看别人的文章详情页的. 此处自己的详情页就是多了两个按钮 : 修改,删除. (前端操作 blog_content.html)

博客详情页要实现的功能:

1. 展示博客详情内容.
2. 展示博客的访问量. (不管是谁访问, 只要访问了该文章, 访问量就 + 1) 仅在后端实现.
2. 展示左侧博客对应的作者的 username

 6.4.1实现详情页

博客详情的请求和响应设计

请求:
{
    get,
    /art/detail
    data:{aid}
}
响应:
{
    "status": 200,
    "message": "",
    "data": [
        {
            "aid": *,
            "title": *****,
            "createtime": ******************,
            "updatetime": ******************,
            "desc": ***************************************,
            "content": *************************************,
            "state": *,
            "uid": *,
            "rcount": **
            "photo": ********************************,
            "nickname": ***",
            "artCount": *
        }
    ]
}

在js包下创建urlutil.js

ajax请求

var aid = getUrlParam("aid"); // 文章id
// 初始化文章的详情信息
    function initArtDetail() {
        jQuery.ajax({
            url: "/art/detail",
            type: "GET",
            data: {
                "aid": aid
            },
            success: function (res) {
                if (res.code == 200 && res.data != null && res.data.aid >= 0) {
                    // 查询文章成功
                    var art = res.data;
                    jQuery("#title").html(art.title);
                    jQuery("#createtime").html(art.createtime);
                    jQuery("#rcount").html(art.rcount);
                    initEdit(art.content); // 设置文章详情
                    if (art.photo != null && art.photo != "") {
                        jQuery("#photo").attr("src", art.photo);
                    }
                    jQuery("#nickname").html(art.nickname);
                    jQuery("#artCount").html(art.artCount);

                } else {
                    alert("抱歉:操作失败,请重试!");
                }
            }
        });
    }

 mapper层(ArticleMapper)

ArticleInfoVO getDetail(@Param("aid") Integer aid);

service层(IArticleService)

ArticleInfoVO getDetail(Integer aid);

service层,impl包(ArticleServiceImpl)

    @Override
    public ArticleInfoVO getDetail(Integer aid) {
        return articleMapper.getDetail(aid);
    }

定义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.blog.dao.ArticleMapper">
    <select id="getDetail" resultType="com.example.blog.model.vo.ArticleInfoVO">
        select a.*,u.photo,u.nickname from articleinfo a
            left join userinfo u on a.uid = u.uid
            where aid=#{aid}
    </select>
</mapper>

controller 层代码 (ArticleController)

    @RequestMapping("/detail")
    public AjaxResult getDetail(Integer aid) {
        //1.参数校验
        if (aid == null || aid <= 0) {
            return AjaxResult.fail(-1,"非法参数");
        }
        //2.查询数据库的文章信息
        ArticleInfoVO articleInfoVO = articleService.getDetail(aid);
        if (articleInfoVO == null || articleInfoVO.getAid() <= 0) {
            return AjaxResult.fail(-2, "文章查询失败!");
        }

        //3.组装数据:查询当前用户总共发表的文章数
        QueryWrapper<ArticleInfo> wrapper = new QueryWrapper<>();
        wrapper.eq("uid", articleInfoVO.getUid());
        articleInfoVO.setArtCount((int) articleService.count(wrapper));
        //4.将最终组装好的数据返回给前端
        return AjaxResult.succ(articleInfoVO);
    }

 

6.4.2 获取当前登录的用户

ajax请求

    var islogin = false; // 是否登录
    // 获取当前登录的用户
    function getSessUser() {
        jQuery.ajax({
            url: "/user/getsess",
            type: "GET",
            data: {},
            success: function (res) {
                if (res.code == 200 && res.data != null && res.data.uid >= 0) {
                    // 当前用户已经登录
                    islogin = true;
                    jQuery("#comment_login_name").html(res.data.nickname);
                    // 判断当前文章是否是当前登录用户发表的
                    isArtByMe(aid);
                } else {
                    // 当前用户未登录
                }
            }
        });
    }

controller 层代码 (UserController)

    @RequestMapping("/getsess")
    public AjaxResult getSess( HttpServletRequest request) {

        UserInfo userInfo = SessionUtil.getUserInfo(request);
        if (userInfo == null || userInfo.getUid() <= 0) {
            return AjaxResult.fail(-2, "当前用户未登录!");
        }

        return AjaxResult.succ(userInfo);
    }

6.4.3 判断当前文章是否属于当前登录用户

 ajax请求

    // 判断当前文章是否属于当前登录用户
    function isArtByMe(aid) {
        jQuery.ajax({
            url: "/user/isartbyme",
            type: "GET",
            data: {
                "aid": aid
            },
            success: function (res) {
                if (res.code == 200 && res.data == 1) {
                    // 当前文章归属于当前登录用户
                    jQuery(".comment_del_class").each(function (i) {
                        jQuery(this).show();
                    });
                }
            }
        });
    }

controller 层代码 (UserController)

    @RequestMapping("/isartbyme")
    public AjaxResult isArtByMe(Integer aid, HttpServletRequest request) {
        if (aid == null || aid <= 0) {
            return AjaxResult.fail(-1, "非法参数");
        }
        UserInfo userInfo = SessionUtil.getUserInfo(request);
        if (userInfo == null || userInfo.getUid() <= 0) {
            return AjaxResult.fail(-2, "当前用户未登录!");
        }
        ArticleInfo articleInfo = articleService.getById(aid);
        if (articleInfo != null && articleInfo.getAid() >= 0
                && articleInfo.getUid() == userInfo.getUid()) {
            return AjaxResult.succ(1);
        }
        return AjaxResult.succ(0);
    }

6.4.4 更新访问量

 ajax请求

    // 更新访问量
    function updateCount() {
        jQuery.ajax({
            url: "/art/update_rcount",
            type: "POST",
            data: {
                "aid": aid
            },
            success: function (res) {
            }
        });
    }

  mapper层(ArticleMapper)

int updateRCount(@Param("aid")Integer aid);

service层(IArticleService)

int updateRCount(Integer aid);

service层,impl包(ArticleServiceImpl)

    @Override
    public int updateRCount(Integer aid) {
        return articleMapper.updateRCount(aid);
    }

定义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.blog.dao.ArticleMapper">
    <update id="updateRCount">
        update articleinfo set rcount = rcount+1 where aid = #{aid}
    </update>
</mapper>

controller 层代码 (ArticleController)

    @RequestMapping("/update_rcount")
    public AjaxResult updateRCount(Integer aid) {
        if(aid == null || aid <= 0) {
            return AjaxResult.fail(-1,"非法参数");
        }

        int result = articleService.updateRCount(aid);
        return AjaxResult.succ(result);
    }

6.4.5 加载评论列表

  ajax请求

    // 加载评论列表
    function initComment() {
        jQuery.ajax({
            url: "/comment/list",
            type: "GET",
            data: {
                "aid": aid
            },
            success: function (res) {
                if (res.code == 200 && res.data != null) {
                    var commentListHtml = "";
                    for (let i = 0; i < res.data.length; i++) {
                        var comment = res.data[i];
                        commentListHtml += '<div style="margin-bottom: 26px;">';
                        commentListHtml += comment.nickname + ':' + comment.content;
                        commentListHtml += '<br>';
                        commentListHtml += '<a class="comment_del_class" style="display: none;" href="javascript:del(' +
                            comment.cid + ')">删除</a>';
                        commentListHtml += '</div>';
                    }
                    jQuery("#commentlist").html(commentListHtml);
                }
            }
        });
    }

 mapper层(CommentMapper)

List<CommentInfoVO> getList(@Param("aid")Integer  aid);

service层(ICommentService)

List<CommentInfoVO> getList(Integer aid);

service层,impl包(CommentServiceImpl)

    @Override
    public List<CommentInfoVO> getList(Integer aid) {
        return commentMapper.getList(aid);
    }

定义CommentMapper.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.blog.dao.CommentMapper">
    <select id="getList" resultType="com.example.blog.model.vo.CommentInfoVO">
        select c.*, u.nickname from commentinfo c
            left join userinfo u on c.uid = u.uid
            where aid = #{aid}
            order by cid desc
    </select>
</mapper>

controller 层代码 (CommentController)

    /**
     * 查询某篇文章下的所有评论
     *
     * @param aid
     * @return
     */
    @RequestMapping("/list")
    public AjaxResult getList(Integer aid) {
        //1.参数校验
        if (aid == null && aid <=0) {
            return AjaxResult.fail(-1,"参数有误!");
        }
        //2.查询数据库
        List<CommentInfoVO> list = commentService.getList(aid);
        //3.将结果返回给前端
        return AjaxResult.succ(list);
    }

6.4.6 添加评论

 ajax请求

    // 添加评论
    function addComment() {
        // 评论正文
        var comment_content = jQuery("#comment_content");
        // 1.非空效验
        if (comment_content.val().trim() == "") {
            alert("请先输入评论的内容!");
            comment_content.focus();
            return false;
        }
        // 2.登录判断
        if (!islogin) {
            alert("请先登录!");
            return false;
        }
        // 3.将前端的数据发送给后端
        //    3.1 文章id
        //    3.2 评论内容
        jQuery.ajax({
            url: "/comment/add",
            type: "POST",
            data: {
                "aid": aid,
                "content": comment_content.val()
            },
            success: function (res) {
                // 4.将后端返回的数据显示给用户
                if (res.code == 200 && res.data == 1) {
                    // 评论添加成功
                    alert("恭喜:评论发表成功!");
                    // 刷新评论(刷新当前页面)
                    location.href = location.href;
                } else {
                    alert("抱歉:评论发表失败,请重试!" + res.msg);
                }
            }
        });
    }

controller 层代码 (CommentController)

    @RequestMapping("/add")
    public AjaxResult add(Integer aid, String content, HttpServletRequest request) {
        if (aid == null || aid <= 0 || !StringUtils.hasLength(content)) {
            return AjaxResult.fail(-1, "参数有误");
        }
        UserInfo userInfo = SessionUtil.getUserInfo(request);
        if (userInfo == null || userInfo.getUid() <= 0) {
            return AjaxResult.fail(-2, "请先登录");
        }

        CommentInfo commentInfo = new CommentInfo();
        commentInfo.setUid(userInfo.getUid());
        commentInfo.setAid(aid);
        commentInfo.setContent(content);
        boolean result = commentService.save(commentInfo);
        return AjaxResult.succ(result?1:0);
    }

6.4.7 删除评论

 ajax请求

    // 评论的删除功能
    function del(cid) {
        if (!confirm("确定删除")) {
            return false;
        }
        // 1.效验参数
        if (cid == "" || cid <= 0) {
            alert("抱歉:操作失败,请刷新页面之后重试!");
            return false;
        }
        if (aid == "" || aid <= 0) {
            alert("抱歉:评论删除失败,请刷新页面之后重试!");
            return false;
        }
        // 2.发送数据给后端[评论id(cid)|文章id(aid)]
        jQuery.ajax({
            url: "/comment/del",
            type: "POST",
            data: {
                "cid": cid,
                "aid": aid
            },
            success: function (res) {
                // 3.将后端结果展现给用户
                if (res.code == 200 && res.data == 1) {
                    // 删除成功
                    alert("恭喜:评论删除成功!");
                    location.href = location.href;
                } else {
                    // 评论删除失败
                    alert("抱歉:评论删除失败!" + res.msg);
                }
            }
        });
    }

controller 层代码 (CommentController)

    @RequestMapping("/del")
    public AjaxResult delete(Long cid, Long aid, HttpServletRequest request) {
        if (cid == null || cid <= 0 || aid == null || aid <= 0) {
            return AjaxResult.fail(-1, "参数有误");
        }
        UserInfo userInfo = SessionUtil.getUserInfo(request);
        if (userInfo == null || userInfo.getUid() <= 0) {
            return AjaxResult.fail(-2, "请先登录!");
        }
        ArticleInfo articleInfo = articleService.getById(aid);
        if (articleInfo == null || articleInfo.getAid() <= 0) {
            return AjaxResult.fail(-3, "非法的文章id");
        }
        if (userInfo.getUid() != articleInfo.getUid()) {
            return AjaxResult.fail(-4, "非法操作");
        }
        boolean result = commentService.removeById(cid);
        return AjaxResult.succ(result?1:0);
    }

6.5 实现删除博客

删除博客功能需要用户登录了才能进行操作, 所以需要过拦截器. 删除博客时需要弹窗是否要删除, 防止用户误点. 并且只有我的博客列比页才有删除功能. 主页的博客列表页是没有的.

 删除博客的请求和响应设计

请求:
{
    post,
    /art/del
    data:{aid}
}
响应:
{
    "status": 200,
    "message": "",
    "data": {
           1 
     }

}

ajax请求

    // 删除文章的方法
    function del(aid) {
        if (confirm("确定删除?")) {
            // 1.效验参数合法性
            if (aid == "" || aid <= 0) {
                alert("非法参数,请重试!");
                return false;
            }
            // 2.调用后端执行删除
            jQuery.ajax({
                url: "/art/del",
                type: "POST",
                data: {
                    "aid": aid
                },
                success: function (res) {
                    // 3.得到后端的结果展示给用户
                    if (res.code == 200 && res.data == 1) {
                        // 删除成功
                        alert("恭喜:删除成功!");
                        location.href = location.href; // 刷新当前页面
                    } else {
                        alert("抱歉:操作失败!" + res.msg);
                    }
                }
            });
        }
    }

controller 层代码 (ArticleController)

    @RequestMapping("/del")
    public AjaxResult delete(Long aid, HttpServletRequest request) {
        if (aid == null || aid <= 0) {
            return AjaxResult.fail(-1,"非法参数");
        }
        UserInfo userInfo = SessionUtil.getUserInfo(request);
        if (userInfo == null || userInfo.getUid() <= 0) {
            return AjaxResult.fail(-2,"当前用户未登录");
        }
        ArticleInfo articleInfo = articleService.getById(aid);
        if (articleInfo == null) {
            return AjaxResult.fail(-1, "非法参数");
        }
        if (articleInfo.getUid() != userInfo.getUid()) {
            return AjaxResult.fail(-3,"没有删除该文章的权限!");
        }
        boolean result =articleService.removeById(aid);
        return AjaxResult.succ(result?1:0);
    }

 

6.6 实现修改博客

修改博客按钮也是在我的博客列表页才有, 主页的列表页没有修改功能. 并且修改功能是需要用户登录才能进行操作的, 所以需要过拦截器.

实现修改博客有两个大的步骤 :
1. 从数据库中查询出文章的内容和标题, 然后初始化在 Markdown 编辑器上.
2. 修改文章内容/标题, 然后进行提交.

 

修改博客的请求和响应设计

请求:
{
    get,
    /art/update
    data:{aid}
}
响应:
{
    "status": 200,
    "message": "",
    "data": {
        1
    }
}

编辑器

// 初始化编辑器
    var editor = editormd("editorDiv", {
        // 这里的尺寸必须在这里设置. 设置样式会被 editormd 自动覆盖掉.
        width: "100%",
        // 设定编辑器高度
        height: "calc(100% - 50px)",
        // 编辑器中的初始内容
        markdown: "# 在这里写下一篇博客",
        // 指定 editor.md 依赖的插件路径
        path: "editor.md/lib/",
        // 加上这个属性, 效果就是把编辑器里面的内容自动给保存到 textarea 里面
        saveHTMLToTextArea: true,
    });

ajax请求

    // 提交
    function mysub() {
        // alert(editor.getValue()); // 获取值
        // editor.setValue("#123") // 设置值

        // 1.参数效验
        var title = jQuery("#title");
        if (title.val().trim() == "") {
            alert("请先输入标题!");
            title.focus();
            return false;
        }
        if (editor.getValue() == "") {
            alert("请先输入正文!");
            return false;
        }
        // 2.将数据发送给后端【文章标题、文章正文、文章id】
        jQuery.ajax({
            url: "/art/update",
            type: "POST",
            data: {
                "aid": aid,
                "title": title.val(),
                "content": editor.getValue()
            },
            success: function (res) {
                // 3.将结果展现给用户
                if (res.code == 200 && res.data == 1) {
                    alert("恭喜:修改成功!");
                    location.href = "/myblog_list.html";
                } else {
                    alert("抱歉:操作失败,请重试!" + res.msg)
                }
            }
        });
    }

controller 层代码 (ArticleController)

    @RequestMapping("/update")
    public AjaxResult update(ArticleInfo articleInfo, HttpServletRequest request) {
        if(articleInfo == null || !StringUtils.hasLength(articleInfo.getTitle()) ||
            !StringUtils.hasLength(articleInfo.getContent())) {
            return AjaxResult.fail(-1, "参数有误!");
        }

        UserInfo userInfo = SessionUtil.getUserInfo(request);
        if(userInfo == null) {
            return AjaxResult.fail(-2, "用户未登录");
        }
        articleInfo.setUid(userInfo.getUid());
        String content = articleInfo.getContent();
        if (content.length() > _MAX_DESC_LENGTH) {
            content = content.substring(0, _MAX_DESC_LENGTH);
        }
        articleInfo.setDesc(content);

        UpdateWrapper<ArticleInfo> wrapper = new UpdateWrapper<>();
        wrapper.eq("aid", articleInfo.getAid());
        boolean result = articleService.update(articleInfo, wrapper);
        return AjaxResult.succ(result?1:0);
    }

6.7 实现发布博客

发布需要登录了才能进行发布博客的操作, 所以需要过拦截器

修改博客的请求和响应设计

请求:
{
    post,
    /art/add
    data:{title, content}
}
响应:
{
    "status": 200,
    "message": "",
    "data": {
        1
    }
}

ajax请求

    initEdit("# 在这里写下一篇博客"); // 初始化编译器的值
    // 提交
    function mysub() {
        // alert(editor.getValue()); // 获取值
        // editor.setValue("#123") // 设置值

        // 1.得到控件,进行非空效验
        var title = jQuery("#title");
        if (title.val().trim() == "") {
            // 标题为空
            alert("请先输入标题!");
            title.focus();
            return false;
        }
        if (editor.getValue() == "") {
            // 内容为空
            alert("请先输入正文!");
            return false;
        }
        // 2.发送请求给后端
        jQuery.ajax({
            url: "/art/add",
            type: "POST",
            data: {
                "title": title.val().trim(),
                "content": editor.getValue()
            }, success: function (res) {
                // 3.将结果返回给前端的用户
                if (res.code == 200 && res.data == 1) {
                    // 文章添加成功
                    if (confirm("恭喜:添加成功!是否继续添加?")) {
                        // 继续添加文章
                        location.href = location.href; // 刷新当前页面
                    } else {
                        // 不添加文章,跳转到文章管理页面
                        location.href = "myblog_list.html"; // 跳转到我的文章管理页
                    }
                } else {
                    // 文章添加失败
                    alert("抱歉:操作失败!" + res.msg);
                }
            }
        });
    }

controller 层代码 (ArticleController)

    /**
     * 添加文章
     * @param articleInfo
     * @return
     */
    @RequestMapping("/add")
    public AjaxResult add(ArticleInfo articleInfo, HttpServletRequest request) {
        if (articleInfo ==null || !StringUtils.hasLength(articleInfo.getTitle()) ||
            !StringUtils.hasLength(articleInfo.getContent())) {
            return AjaxResult.fail(-1, "非法参数");
        }
        UserInfo userInfo = SessionUtil.getUserInfo(request);

        if(userInfo == null || userInfo.getUid() <= 0) {
            return AjaxResult.fail(-2, "请先登录");
        }
        articleInfo.setUid(userInfo.getUid());
        String desc = articleInfo.getContent();
        if(desc.length() > _MAX_DESC_LENGTH ) {
            desc = desc.substring(0, _MAX_DESC_LENGTH);
        }
        articleInfo.setDesc(desc);
        boolean result = articleService.save(articleInfo);
        return AjaxResult.succ(result?1:0);
    }

6.8 实现所有人的文章列表的分页功能

使用MyBatisPlus分页功能

 实现所有人的文章的请求和响应设计

请求:
{
    get,
    /art/list
    data:{pindex, psize}
}
响应:
{
    "status": 200,
    "message": "",
    "data": {
        "records": [
            {
                "aid": *,
                "title": ***,
                "createtime": ********************,
                "updatetime": *********************",
                "desc": ****************************************,
                "content": ****************************************,
                "state": *,
                "uid": *,
                "rcount": *
            },
        ],
        "total": *,
        "size": *,
        "current": *,
        "orders": [],
        "optimizeCountSql": true,
        "searchCount": true,
        "maxLimit": null,
        "countId": null,
        "pages": *
    }

}

ajax请求

    // 每页显示条数
    var psize = 4;
    // 当前页码(当前在第几页)
    var pindex = getUrlParam("pindex");
    if (pindex == "") { // 修正当前页码
        pindex = 1;
    }
    // 分页最大页面
    var pages = 1;

    // 访问后端接口,获取列表数据
    function initPageList() {
        jQuery.ajax({
            url: "/art/list",
            type: "GET",
            data: {
                "pindex": pindex,
                "psize": psize
            },
            success: function (res) {
                if (res.code == 200 && res.data.records.length >= 0) {
                    // 查询到了数据,将数据展示在页面的列表中
                    pages = res.data.pages; // 分页最大条数
                    var listHtml = "";
                    for (let i = 0; i < res.data.records.length; i++) {
                        var art = res.data.records[i];

                        listHtml += '<div class="blog">';
                        listHtml += '<div class="title">' + art.title + '</div>';
                        listHtml += '<div class="date">' + art.createtime + '</div>';
                        listHtml += '<div class="desc">' + art.desc + '</div>';
                        listHtml += '<a href="blog_content.html?aid=' + art.aid + '" class="detail">查看全文 &gt;&gt;</a>';
                        listHtml += '</div>';
                    }
                    jQuery("#artlist_div").html(listHtml);
                } else if (res.code == 200 && res.data.length == 0) {
                    // 接口查询成功,但数据库中没有数据
                    pages = 1; // 分页最大条数
                    jQuery("#artlist_div").html("<h3 style='margin-left: 50px;margin-top: 50px;'>暂无数据!</h3>");
                } else {
                    alert("抱歉:查询失败!" + res.msg);
                }
            }
        });
    }

    initPageList();


    // 点击“首页”
    function doFirst() {
        // 判断是否已经在首页
        if (pindex <= 1) {
            alert("已经在首页了,不能再调转了。");
            return false;
        }
        // 跳转到首页
        location.href = "blog_list.html";
    }

    // 点击“末页”
    function doLast() {
        // 判断是否已经在末页
        if (pindex >= pages) {
            alert("已经在末页了,不能跳转了。");
            return false;
        }
        // 跳转到末页
        location.href = "blog_list.html?pindex=" + pages;
    }

    // 点击“上一页”
    function doBefore() {
        // 判断是否已经在首页
        if (pindex <= 1) {
            alert("已经在首页了,不能再调转了。");
            return false;
        }
        pindex = parseInt(pindex) - 1;
        // 跳转到上一页
        location.href = "blog_list.html?pindex=" + pindex;
    }

    // 点击“下一页”
    function doNext() {
        // 判断是否已经在末页
        // 判断是否已经在末页
        if (pindex >= pages) {
            alert("已经在末页了,不能跳转了。");
            return false;
        }
        pindex = parseInt(pindex) + 1;
        // 跳转
        location.href = "blog_list.html?pindex=" + pindex;
    }

controller 层代码 (ArticleController)

    @RequestMapping("/list")
    public AjaxResult getList(Integer pindex, Integer psize) {
        if(pindex == null || pindex <= 0) {
            pindex = 1;
        }
        if(psize == null || pindex <= 0) {
            psize = PAGE_PAGE_SIZE;
        }
        Page page = new Page(pindex, psize);
        QueryWrapper<ArticleInfo> wrapper = new QueryWrapper<>();
        wrapper.orderByDesc("aid");
        Page<ArticleInfo> result = articleService.page(page,wrapper);
        return AjaxResult.succ(result);
    }

6.9 个人中心 - 实现修改用户头像、用户名或密码

6.9.1 修改用户头像

想要修改头像, 那么就得先获取数据库中原来的头像, 此处顺便将原用户名一起获取并展示出来. 原密码最好不要获取出来, 因为有可能你在修改密码的中途, 你去上厕所了, 然后你的密码被你好兄弟给修改了. 

 获取用户头像和昵称的请求和响应设计

请求:
{
    get,
    /user/getsess
    data:{}
}
响应:
{
    "status": 200,
    "message": "",
    "data": {
        "uid": *,
        "loginname": ****,
        "nickname": ****,
        "password": *,
        "github": ****,
        "photo": ********************,
        "state": *
    }

}

ajax请求

    // 获取用户头像和昵称
    function initPage() {
        jQuery.ajax({
            url: "/user/getsess",
            type: "GET",
            data: {},
            success: function (res) {
                if (res.code == 200 && res.data != null && res.data.uid >= 0) {
                    // 得到当前的登录用户
                    var userinfo = res.data;
                    if (null != userinfo.photo && userinfo.photo != "") {
                        jQuery("#photo").attr("src", userinfo.photo);
                    }
                    jQuery("#nickname").val(userinfo.nickname);
                } else {
                    alert("抱歉:查询用户信息出错,请刷新页面再试!" + res.msg);
                }
            }
        });
    }

controller 层代码 (UserController)

    @RequestMapping("/getsess")
    public AjaxResult getSess( HttpServletRequest request) {

        UserInfo userInfo = SessionUtil.getUserInfo(request);
        if (userInfo == null || userInfo.getUid() <= 0) {
            return AjaxResult.fail(-2, "当前用户未登录!");
        }

        return AjaxResult.succ(userInfo);
    }
6.9.2 实现保存头像

此处我们上传新的头像后, 并点击保存按钮时, 就是修改头像成功了

1. 此处的得到图片代码比较特殊

2. 发送 ajax 时, 参数是发送一个 form 表单给后端, 所以 ajax 中需要多添加两个参数 : processData 和 contentType.

3. 表单传给后端时, 后端针对图片生成一个网络地址映射到本地保存的地址, 然后将网络地址返回给前端, 前端操作 dom 树将其设置给 photo 的 src 属性.

保存头像的请求和响应设计

请求:
{
    post,
    /user/save_photo
    data:{}
}
响应:
{
    "status": 200,
    "message": "",
    "data": {
        /image/***
    }

}

ajax请求

    // 保存头像
    function savePhoto() {
        // 得到图片
        var photo = jQuery("#file")[0].files[0];
        if (photo == null) {
            alert("请先选择要上传的头像!");
            return false;
        }
        // 构建一个 form 表单
        var formData = new FormData();
        formData.append("file", photo);
        jQuery.ajax({
            url: "/user/save_photo",
            type: "POST",
            data: formData,
            processData: false,
            contentType: false,
            success: function (res) {
                if (res.code == 200 && res.data != null && res.data != "") {
                    // 图片上传成功
                    jQuery("#photo").attr("src", res.data);
                } else {
                    // 图片上传失败
                    alert("抱歉:上传图片失败,请重试!" + res.msg);
                }
            }
        });
    }

在配置文件中 application.properties 指定保存头像的本地路径 :

imagepath=D:/image/

在添加拦截规则的类里边加上 addResourceHandlers 类 :

在添加拦截规则的类里边加上 addResourceHandlers 类 :

@Value("${imagepath}")
private String imagepath;
**
 * 映射图片路径
 * @param registry
 */
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/image/**")
            .addResourceLocations("file:" + imagepath + "/");
}

controller 层代码 (UserController)

    @RequestMapping("/save_photo")
    public AjaxResult savePhoto(MultipartFile file, HttpServletRequest request) {
        String imagreType = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));

        String imgName = UUID.randomUUID().toString().replace("-", "") + imagreType;
        String url = "/image/"+imgName;
        try {
            file.transferTo(new File(imagepath+imgName));
        } catch (IOException e) {
            return AjaxResult.fail(-1,"图片上传失败");
        }
        UserInfo userInfo = SessionUtil.getUserInfo(request);
        if (userInfo == null || userInfo.getUid() <= 0) {
            return AjaxResult.fail(-2, "当前用户未登录!");
        }
        UpdateWrapper<UserInfo> wrapper = new UpdateWrapper<>();
        wrapper.set(true, "photo", url);
        wrapper.eq("uid", userInfo.getUid() );
        boolean result = userService.update(wrapper);
        if (result) {
            userInfo.setPhoto(url);
            HttpSession session = request.getSession();
            session.setAttribute(AppVar.SESSION_KEY_USERINFO,userInfo);
            return AjaxResult.succ(url);
        }else {
            return AjaxResult.fail(-3, "数据库修改失败");
        }

    }

【步骤分析】

1. 保存图片到服务器

当用户从自己的电脑上选取了一张图片上传时, 后端就需要将用户传来的图片保存到服务器, 然后再生成图片对应的网络地址. 最后将新头像对应的网络地址返回给前端, 前端就可以通过设置头像 photo 对应的 src 属性为返回的网络地址.

2. 保存图片到数据库

修改头像不仅要更新当前页面展示的头像, 数据库中的头像对应的网络地址也要更新, 另外如果更新数据成功了, 要同时更新 session 中的 userinfo 信息. 因为博客列表页的当前用户的身份信息, 后端都是从 session 中取出来返回给前端的, 所以需要同时更新数据库和 session 中的 photo 字段(属性).

 
6.9.3 修改用户名或密码


此处我们点击个人中心跳转到修改用户信息的页面时, 它只是将头像和用户名展示出来了, 而原密码, 新密码和确认密码需要手动输入(要么三个都不为空 - 改, 要么都为空 - 不改).

如果不修改密码, 只是修改用户名或者都不修改, 然后点击修改按钮, 就提示修改成功, 并跳转到我的内容管理页面.
如果修改了密码, 并点击了修改按钮, 就提示修改成功, 并强制用户重新登录.

 获取用户头像和昵称的请求和响应设计

请求:
{
    post,
    /user/update
    data:{nickname,oldpassword,password,isUpdatePassword}
}
响应:
{
    "status": 200,
    "message": "",
    "data": {
        1
    }

}

ajax请求

    // 修改昵称或修改密码
    function updateUser() {
        var isUpdatePassword = false; // 是否修改密码
        // 1.非空效验
        var nickname = jQuery("#nickname");
        var oldPassword = jQuery("#old_password");
        var password = jQuery("#password");
        var password2 = jQuery("#password2");
        if (nickname.val().trim() == "") {
            alert("请先输入昵称!");
            nickname.focus();
            return false;
        }
        if (oldPassword.val() != "" ||
            password.val() != "" || password2.val() != "") {
            // 需要修改密码
            isUpdatePassword = true;
            if (oldPassword.val().trim() == "") {
                alert("请先输入原密码!");
                oldPassword.focus();
                return false;
            }
            if (password.val().trim() == "") {
                alert("请先输入新密码!");
                password.focus();
                return false;
            }
            // 密码强度效验(密码的长度必须大于 8)【扩展,密码组合强度效验】
            if(password.val().length<8){
                alert("抱歉:密码强度太低,密码位数必须大于等于8位!");
                password.focus();
                return false;
            }
            if (password2.val().trim() == "") {
                alert("请先输入确认密码!");
                password2.focus();
                return false;
            }
            // 判断新密码和确认密码是否一致
            if (password.val() != password2.val()) {
                alert("两次输入的新密码不一致,请先确认!");
                return false;
            }
        }
        // 2.将前端的数据提交给后端
        jQuery.ajax({
            url: "/user/update",
            type: "POST",
            data: {
                "nickname": nickname.val(),
                "oldpassword": oldPassword.val(),
                "password": password.val(),
                "isUpdatePassword": isUpdatePassword
            },
            success: function (res) {
                // 3.将返回的结果展现给用户
                if (res.code == 200 && res.data == 1) {
                    // 修改成功
                    if (isUpdatePassword) {
                        alert("恭喜:修改成功,请重新登录!");
                        // 修改密码,重新登录
                        location.href = "login.html";
                    } else {
                        alert("恭喜:昵称修改成功!");
                    }
                } else {
                    // 修改失败
                    alert("抱歉:修改失败,请重试!" + res.msg);
                }
            }
        });
    }

controller 层代码 (UserController)

    @RequestMapping("/update")
    public AjaxResult update(String nickname, String oldpassword, String password,
                             Boolean isUpdatePassword, HttpServletRequest request) {
        if (!StringUtils.hasLength(nickname)) {
            return AjaxResult.fail(-1,"非法参数");
        }
        if (isUpdatePassword) {
            if (!StringUtils.hasLength(oldpassword) ||
                    !StringUtils.hasLength(password)) {
                return AjaxResult.fail(-1,"非法参数");
            }
        }
        UserInfo userInfo = SessionUtil.getUserInfo(request);
        if (userInfo == null || userInfo.getUid() <= 0) {
            return AjaxResult.fail(-2, "当前用户未登录!");
        }
        UpdateWrapper<UserInfo> wrapper = new UpdateWrapper<>();
        if (isUpdatePassword) {
            if (PasswordUtil.decrypt(oldpassword, userInfo.getPassword())) {
                return AjaxResult.fail(-3,"原密码输入错误!");
            }
            password = PasswordUtil.encrypt(password);
            wrapper.set("password",password);
        }



        wrapper.set("nickname",nickname);
        wrapper.eq("uid", userInfo.getUid());
        boolean result = userService.update(wrapper);
        if (result) {
            userInfo.setNickname(nickname);
            HttpSession session = request.getSession();
            session.setAttribute(AppVar.SESSION_KEY_USERINFO, userInfo);
        }
        return AjaxResult.succ(result?1:0);
    }

【步骤分析】

1. 非空效验

        前端传递了新用户名, 原密码, 新密码, 以及是否修改了密码的标志, 于是在做判断时, 如果只修改了用户名, 就可以使用 isUpdatePassword 标志位来跳过更新数据密码的操作. 否则都要进行修改.

2. 组装数据

       组装好一个新的 userinfo (新的用户名或密码), 为更新数据库操作提供数据源, 此处更新密码成功的前提是原密码和数据库密码要保持一致, 而数据库中存储的是加密后的密码, 所以需要先拿着原密码和数据库中的密码去调用解密方法, 得到一个 boolean 类型的值, 再根据这个布尔值来判断是否要进行修改操作.

【注意】session 中的对象存储机制 >>

      由于我们是可以拿到当前用户的  session, 所以想要拿数据库中存储的密码, 我就会想着去 session 中拿到 userinfo, 再去拿到对应的密码, 这确实挺方便. 但是我在实现登录页面时, 登录成功后并将 session 存储 redis, 但是在返回数据给前端之前, 我执行了将密码置为空字符串这一操作, 因为密码如果通过网络传输返回给前端, 是不安全的. 

        <问题的出处>  正因为我的这一步置空字符串操作, 就会导致 session 中的密码也变成了空字符串. 这是为什么呢 ??

因为 session 的底层是用 concurrentHashMap 来保存数据的, 而 map 中并没有直接存储新的对象, 而是存储了对象的引用, 也就是 userinfo 的引用, 虽然我是在设置密码为空之前就将 userinfo 存储 session 了, 但是这也同样影响了 session 中的 password 了. 此时 session 中的 password 已经是空字符串了.

       再回到调用解密方法这一步, 我们就不能拿着原密码和 session 中的密码去调用解密码方法了, 而是需要拿着从 session 获取到的 userinfo 中的用户 id, 去查数据库得到一个新的 userinfo, 此时这个 userinfo 的密码才不为空, 才可以拿着它的 password 去和原密码去调用解密方法.

3. 修改数据库

        经过了第二步的组装数据, 第三步就变得简单了, 只需要使用 MP 进行修改操作即可, 但是在进行修改操作时, 修改后的用户名或密码最好设置在 updatewrapper 中, 然后只传一个 wrapper 对象. 如果将修改后的用户名或密码设置给 session 中的 userinfo, 然后再给 MP 传两个参数 (userinfo, wrapper), 那么有可能造成不必要的参数覆盖问题.

       另外就是修改完数据库之后, 要及时更新 session 中的用户名, 因为如果只修改了用户名, 不修改密码, 修改完成后会跳转博客列表页, 而博客列表页的用户身份信息都是从后端的 session 中来的, 如果不及时更新 session 的话, 那么在你下次重新登录之前, 用户名都不会变.

 7. 项目扩展功能(亮点)

1. ⽂章草稿保存功能
2. ⽂章定时发布功能
3. ⽤户多次登录,账号冻结的业务
4. 找回密码功能(可通过邮件或短信验证码)
5. ⽂章点赞/踩
6. 注册成功之后,发送欢迎邮件

8. 框架技术升级

1. 引⼊ Validator 框架,验证参数
2. 添加 XXL-Job 实现⽂章定时发布
3. 引⼊消息队列,登录时记录⽇志和安全验证(登录次数 + IP ⿊名单 + 异地登录等)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值