SSM框架Demo: 简朴博客系统

文章目录

  • 1. 前端页面效果
  • 2. 项目创建
  • 3. 前期配置
    • 3.1. 创建数据库数据表
    • 3.2. 配置文件
  • 4. 创建实体类
  • 5. 统一处理
    • 5.1. 统一返回格式处理
    • 5.2. 统一异常处理
  • 6. 全局变量
  • 7. Session工具类
  • 8. 登录拦截器
  • 9. 密码加盐加密
  • 10. 线程池组件
  • 11. dao层
    • 11.1. UserMapper
    • 11.2. ArticleMapper
  • 12. 服务层service
    • 12.1. UserService
    • 12.2. ArticleService
  • 13. 核心—控制层controller
    • 13.1. UserController
      • 13.1.1. 注册功能
      • 13.1.2. 登录功能
      • 13.1.3. 注销功能
      • 13.1.4. 判断当前用户是否登录
    • 13.2. ArticleController
      • 13.2.1. 返回当前登录用户的文章列表
      • 13.2.2. 删除文章功能
      • 13.2.3. 查看文章详情功能
      • 13.2.4. 更新文章阅读量
      • 13.2.5. 添加文章
      • 13.2.6. 修改文章
        • 13.2.6.1. 页面初始化
        • 13.2.6.2. 发布修改后的文章
      • 13.2.7. 根据分页来查询汇总列表
  • 14. Session升级存储到Redis
  • 15. 项目部署
  • 16. 项目亮点

简朴博客系统:简单朴素…

1. 前端页面效果

♨️注册页

包含以下用户信息:

  1. 用户名
  2. 密码

img

♨️登录页

包含以下用户信息:

  1. 用户名
  2. 密码

img

♨️文章详情页

登录状态下“登录”按钮变为“注销”按钮。

包含以下用户信息:

  1. 博文作者 id
  2. 代码仓库链接
  3. 文章总数

包含以下博文信息:

  1. 作者 id
  2. 文章 id
  3. 标题
  4. 时间
  5. 正文
  6. 阅读量

img

♨️个人博客列表页

包含以下博文信息:

  1. 标题
  2. 时间
  3. 摘要

img

♨️文章汇总列表页

登录状态下“登录”按钮变为“注销”按钮。

包含以下博文信息:

  1. 标题
  2. 时间
  3. 摘要

img

♨️博客编辑页

包含以下用户信息:

  1. 用户id

包含以下博文信息:

  1. 作者 id,即当前用户 id
  2. 标题
  3. 正文
  4. 创建时间,即提交时的时间
  5. 自动生成的文章 id

img

2. 项目创建

使用 Spring 全家桶 + MyBatis 框架进行开发。

img

创建项目目录:

controller,前后端交互控制器,接收请求,处理请求,调用 service,将响应返回给前端。

service,调用数据持久层 dao 层。

dao,进行数据库操作。

model,实体类。

common,公共类,Utils 工具类。

config,配置类。

img

3. 前期配置

当我们创建完一个 Spring 项目之后我们首先就是要准备好相关的配置文件以及创建好数据库。

3.1. 创建数据库数据表

创建数据库

-- 创建数据库
drop database if exists mycnblog;
create database mycnblog DEFAULT CHARACTER SET utf8mb4;-- 选中数据库
use mycnblog;

包含以下两张表:

  1. userinfo 用户表
  2. articleinfo 文章表

🎯userinfo

  1. id,用户 id(主键)
  2. username,用户名
  3. password,密码
  4. photo,头像
  5. createtime,创建时间
  6. updatetime,更新时间
  7. state 状态(预留字段)
-- 创建表用户表
drop table if exists  userinfo;
create table userinfo(id int primary key auto_increment,username varchar(100) not null unique,password varchar(100) not null,photo varchar(500) default '',createtime datetime default now(),updatetime datetime default now(),`state` int default 1
) default charset 'utf8mb4';

🎯articleinfo

  1. id,文章 id(自增主键)
  2. title,标题
  3. content,正文
  4. createtime,创建时间
  5. updatetime,更新时间
  6. uid,用户 id
  7. rcount,阅读量
  8. state 状态(预留字段)
-- 创建文章表
drop table if exists  articleinfo;
create table articleinfo(id int primary key auto_increment,title varchar(100) not null,content text not null,createtime datetime default now(),updatetime datetime default now(),uid int not null,rcount int not null default 1,`state` int default 1
)default charset 'utf8mb4';

初始数据:

-- 添加一个用户信息
INSERT INTO `mycnblog`.`userinfo` (`id`, `username`, `password`, `photo`, `createtime`, `updatetime`, `state`) VALUES 
(1, 'admin', 'admin', '', '2023-11-06 17:10:48', '2023-11-06 17:10:48', 1);-- 文章添加测试数据
insert into articleinfo(title,content,uid)values('Java','Java正文',1);

3.2. 配置文件

# 配置数据库的连接字符串
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mycnblog2023?characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=111111
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 配置打印 MyBatis 执行的 SQL
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# 配置打印 MyBatis 执行的 SQL
logging.level.com.example.demo=debug
# 设置时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm
spring.jackson.time-zone=GMT+8
# session 过期时间
server.servlet.session.timeout=1800
spring.session.redis.flush-mode=on_save
spring.session.redis.namespace=spring:session

接着将前端文件导入static中。

获取链接:链接

img

此时就项目就创建完成并且连接上 Mysql 数据库了,接下来就是去实现相关的代码了。

img

4. 创建实体类

🍂model.UserInfo类,对应着数据库中的 userinfo 这张表。

package com.example.demo.model;import lombok.Data;import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;import org.springframework.util.DigestUtils;@Data
public class Userinfo implements Serializable {private int id;private String username;private String password;private String photo;private LocalDateTime createtime;private LocalDateTime updatetime;private int state;}

🍂model.ArticleInfo类,对应着数据库中的 articleInfo 这张表。

package com.example.demo.model;import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Date;@Data
public class Articleinfo implements Serializable {private int id;private String title;private String content;@JsonFormat(pattern = "yyyy-MM-dd HH:mm", timezone = "GMT+8")private LocalDateTime createtime;private LocalDateTime updatetime;private int uid;private int rcount; // 文章阅读量private int state;
}

🍂model.vo.UserInfoVO扩展类,对于一些特殊情况,特殊处理,可以在这里面增加属性,不是增加在原类里,而数据库的表是没有变化的。

package com.example.demo.model.vo;import com.example.demo.model.Userinfo;
import lombok.Data;import java.time.LocalDateTime;/*** userinfo 扩展类*/
@Data
public class UserinfoVO extends Userinfo {private String checkCode;private int artCount; // 用户文章数
}

5. 统一处理

统一处理可以让代码更加低耦合,高内聚,更符合单一设计原则。

5.1. 统一返回格式处理

🍂统一格式类 common.ResultAjax

该类实现了Serializable接口,实现了这个接口表示该类的对象可以通过序列化机制转换为字节流,并且可以在网络上传输、存储到文件中或在不同的 Java 虚拟机之间进行传递;序列化是将对象转换为字节序列的过程,反序列化则是将字节序列转换回对象的过程。

  1. 该类包括我们的状态码,状态码的描述信息,以及返回的数据。
  2. 该类重载了一些静态的返回方法分别表示我们返回成功或者返回失败的情况。
package com.example.demo.common;import lombok.Data;/*** 前后端交互的统一数据格式对象*/
@Data
public class ResultAjax {private int code; // 状态码private String msg; // 状态描述信息private Object data; // 交互数据// 成功public static ResultAjax succ(Object data) {ResultAjax result = new ResultAjax();result.setCode(200);result.setMsg("");result.setData(data);return result;}public static ResultAjax succ(int code, String msg, Object data) {ResultAjax result = new ResultAjax();result.setCode(code);result.setMsg(msg);result.setData(data);return result;}// 失败public static ResultAjax fail(int code, String msg) {ResultAjax result = new ResultAjax();result.setCode(code);result.setMsg(msg);result.setData(null);return result;}public static ResultAjax fail(int code, String msg, Object data) {ResultAjax result = new ResultAjax();result.setCode(code);result.setMsg(msg);result.setData(data);return result;}}

🍂统一返回处理器 common.ResponseAdvice

准备好了统一返回的类,为了以防止返回的数据不是规定格式(百密一疏的缺漏情况),可以写一个保底处理类,而正常写的时候,返回数据都是自己调用ResultAjax类的包装方法。

  1. 该类实现了ResponseBodyAdvice接口允许在返回数据之前对返回的数据进行校验和修改。
  2. 需要对String类型的数据进行了特殊的处理,String 类型不同与我们一般的类型,需要注入ObjectMapper对象手动将 String 类型转换成 json 格式。
package com.example.demo.common;import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;/*** 保底统一返回值处理*/
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Autowiredprivate ObjectMapper objectMapper;@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {if (body instanceof ResultAjax) {return body;}if (body instanceof String) {ResultAjax resultAjax = ResultAjax.succ(body);try {return objectMapper.writeValueAsString(resultAjax);} catch (JsonProcessingException e) {e.printStackTrace();}}return ResultAjax.succ(body);}
}

5.2. 统一异常处理

🍂common.ExceptionAdvice

package com.example.demo.common;import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;/*** 统一异常处理*/
@RestControllerAdvice
public class ExceptionAdvice {@ExceptionHandler(Exception.class)public ResultAjax doException(Exception e) {return ResultAjax.fail(-1, e.getMessage());}
}

6. 全局变量

🍂common.AppVariable

此类存放我们的全局变量,只放了一个固定的session key

package com.example.demo.common;/*** 全局变量*/
public class AppVariable {// 用户 session keypublic static final String SESSION_USERINFO_KEY = "SESSION_USERINFO";}

7. Session工具类

🍂common.SessionUtils

该类主要是判断服务器是否存储了用户的Session,然后将此 Session 中的 userinfo 给取出来然后返回;这里用的是一个静态的方法这就方便我们的调用了,用的时候不需要去注入或者 new 了。

package com.example.demo.common;import com.example.demo.model.Userinfo;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;/*** session 工具类*/
public class SessionUtils {/*** 得到当前登录用户*/public static Userinfo getUser(HttpServletRequest request) {HttpSession session = request.getSession(false);if (session != null &&session.getAttribute(AppVariable.SESSION_USERINFO_KEY) != null) {// 登录状态return (Userinfo) session.getAttribute(AppVariable.SESSION_USERINFO_KEY);}return null;}}

8. 登录拦截器

在进入一个页面的时候可能需要用户的登录权限,所以应该对请求做一个拦截,对权限进行校验。

拦截处理如下:

我们首先需要获取当前的 Session,然后判断有没有存储指定的 Session,如果存在的话那就返回 true 意味着继续执行后续的代码,如果不是那就直接跳转到登录的页面,后续的代码自然也就不执行了返回 false。

🍂拦截器 config.LoginIntercept

package com.example.demo.config;import com.example.demo.common.AppVariable;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;/*** 用户拦截器*/
public class LoginIntercept implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {HttpSession session = request.getSession(false);if (session != null && session.getAttribute(AppVariable.SESSION_USERINFO_KEY) != null) {// 用户已登录return true;}// 登录页面response.sendRedirect("/login.html");return false;}
}

🍂拦截规则配置类 config.AppConfig

拦截并不是对所有的请求都去进行拦截,我们会拦截部分的请求然后同样的也是会放开一些的请求,此类就是用来处理我们需要拦截哪些放开哪些东西,并且我们加入Congiguration的注解会随着框架的启动而生效。

package com.example.demo.config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** 系统配置文件*/
@Configuration
public class MyConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginIntercept()).addPathPatterns("/**").excludePathPatterns("/user/login").excludePathPatterns("/user/reg").excludePathPatterns("/user/isLogin").excludePathPatterns("/art/getlistbypage").excludePathPatterns("/art/detail").excludePathPatterns("/editor.md/**").excludePathPatterns("/art/increment_rcount").excludePathPatterns("/img/**").excludePathPatterns("/js/**").excludePathPatterns("/css/**").excludePathPatterns("/blog_list.html").excludePathPatterns("/blog_content.html").excludePathPatterns("/reg.html").excludePathPatterns("/login.html");}
}

9. 密码加盐加密

🍁为什么要加密

如果密码没有进行加密一旦被拖库了是非常的危险的,用户信息特别是密码会全部被别人获取,想要防止密码被人看到就得对密码进行加密然后将加密后的密码存入数据库中去,这样即使数据库的密码被得到了也不能知道原密码是多少。

🍁md5

md5 是一种比较常用的加密方式,它是将任意长度的输入通过一个算法然后生成一个 128 位的的二进制值输出,通常情况下是用 32 位的 16 进制来表示,其特点其加密是不可逆的,即加密之后不能通过加密值推测出原始值。

🍁md5 缺点

md5 的加密虽然是不可逆的,但还是有一个问题是我们每次对同一个密码加密其得到的结果是固定的值,那么如果可以以穷举出所有的字符的话那么就可以推测出所有的密码了,这就是我们的彩虹表,彩虹表里面以类似键值对的方式将原始值以及 md5 的加密值存储起来然后不断的去完善这个彩虹表,这样对于绝大多数的密码我们都可以通过彩虹表来找到的,这就存在一定的风险了。

🍁加盐加密原理

加盐算法可以解决 md5 被暴力破解的问题,在用 md5 算法对密码进行加密的时会给原密码加上一个全球不重复的随机的盐值(UUID),这样即使是同一个密码两次不同的加密在加盐之后生成的加密密码也是不一样的,就大大增加了密码破译的成本,更进一步保证了数据。

🎯后端的盐值拼接约定

  1. 盐值跟原始密码直接拼接后进行md5加密。
  2. 盐值跟生成的加密密码直接以$拼接[salt]$[plus password]保存到数据库。

🎯验密过程

  1. 根据$分隔符获取到 [盐值] 和 [加密密码]。
  2. [盐值] 和 [待验证的密码] 拼接后进行 md5 生成[待验证的加密密码]。
  3. 对比 [正确的加密密码] 和 [待验证的加密密码]。

🍂common.PasswordUtils

package com.example.demo.common;import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;import java.nio.charset.StandardCharsets;
import java.util.UUID;/*** 密码工具类*/
public class PasswordUtils {/*** 加盐加密*/public static String encrypt(String password) {// 1. 生成盐值String salt = UUID.randomUUID().toString().replace("-", "");// 2. 将盐值 + 密码进行 md5 加密得到最终密码String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes(StandardCharsets.UTF_8));// 3. 将盐值和最终密码拼接成字符串 (以$分隔) 进行返回return salt + "$" + finalPassword;}/*** 加盐验证*/public static boolean decrypt(String password, String dbPassword) {if (!StringUtils.hasLength(password) || !StringUtils.hasLength(dbPassword) ||dbPassword.length() != 65) {return false;}// 1. 得到盐值String[] dbPasswordArray = dbPassword.split("\\$");if (dbPasswordArray.length != 2) {return false;}// 盐值String salt = dbPasswordArray[0];// 最终正确密码String dbFinalPassword = dbPasswordArray[1];// 2. 加密待验证的密码String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes(StandardCharsets.UTF_8));// 3. 对比验证if (finalPassword.equals(dbFinalPassword)) {return true;}return false;}
}

10. 线程池组件

在容器中注入一个线程池,后续的业务逻辑可以使用线程提高执行效率。

🍂config.ThreadPoolConfig

package com.example.demo.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;/*** 线程池组件*/
@Configuration
public class ThreadPoolConfig {@Beanpublic ThreadPoolTaskExecutor taskExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(5);executor.setMaxPoolSize(10);executor.setQueueCapacity(10000);executor.setThreadNamePrefix("MyThread-");executor.initialize();return executor;}
}

11. dao层

11.1. UserMapper

UserMapper 里面存放着关于 userinfo 表的接口,使用 mybatis 注解的方式实现 sql 的映射。

package com.example.demo.dao;import com.example.demo.model.Userinfo;
import com.example.demo.model.vo.UserinfoVO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;public interface UserMapper {// 将注册用户的密码保存到数据库中@Insert("insert into userinfo(username,password) values(#{username},#{password})")
//    @Insert("insert into userinfo(username,password,updatetime) values(#{username},#{password},null)")int reg(Userinfo userinfo);// 根据用户名查询用户对象@Select("select * from userinfo where username=#{username}")Userinfo getUserByName(@Param("username") String username);// 根据用户 id 查询用户对象@Select("select * from userinfo where id=#{uid}")UserinfoVO getUserById(@Param("uid") int uid);}

11.2. ArticleMapper

ArticleMapper 里面存放着关于 articleinfo 表的接口,使用 mybatis 注解的方式实现 sql 的映射。

package com.example.demo.dao;import com.example.demo.model.Articleinfo;
import org.apache.ibatis.annotations.*;import java.util.List;public interface ArticleMapper {// 根据用户 id 查询此用户发表的所有文章@Select("select * from articleinfo where uid=#{uid} order by id desc")List<Articleinfo> getListByUid(@Param("uid") int uid);// 判断文章的归属人+删除文章操作@Delete("delete from articleinfo where id=#{aid} and uid=#{uid}")int del(@Param("aid") Integer aid, int uid);// 添加文章到数据库@Insert("insert into articleinfo(title,content,uid) values(#{title},#{content},#{uid})")
// @Insert("insert into articleinfo(title,content,uid,updatetime) values(#{title},#{content},#{uid},null)")int add(Articleinfo articleinfo);// 修改文章中间步骤: 查询自己发表的文章详情@Select("select * from articleinfo where id=#{aid} and uid=#{uid}")Articleinfo getArticleByIdAndUid(@Param("aid") int aid, @Param("uid") int uid);// 修改文章, 并效验归属人@Update("update articleinfo set title=#{title},content=#{content} where id=#{id} and uid=#{uid}")int update(Articleinfo articleinfo);// 根据文章 id 查询文章对象@Select("select * from articleinfo where id=#{aid}")Articleinfo getDetailById(@Param("aid") int aid);// 根据 uid 查询用户发表的总文章数@Select("select count(*) from articleinfo where uid=#{uid}")int getArtCountByUid(@Param("uid") int uid);// 更新文章阅读量@Update("update articleinfo set rcount=rcount+1 where id=#{aid}")int incrementRCount(@Param("aid") int aid);// 查询一页的文章列表@Select("select * from articleinfo order by id desc limit #{psize} offset #{offset}")public List<Articleinfo> getListByPage(@Param("psize") int psize, @Param("offset") int offset);// 查询文章表记录数@Select("select count(*) from articleinfo")int getCount();}

12. 服务层service

12.1. UserService

package com.example.demo.service;import com.example.demo.dao.UserMapper;
import com.example.demo.model.Userinfo;
import com.example.demo.model.vo.UserinfoVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class UserService {@Autowiredprivate UserMapper userMapper;// 将注册用户的密码保存到数据库中public int reg(Userinfo userinfo) {return userMapper.reg(userinfo);}// 根据用户名查询用户对象public Userinfo getUserByName(String username) {return userMapper.getUserByName(username);}// 根据用户 id 查询用户对象public UserinfoVO getUserById(int uid) {return userMapper.getUserById(uid);}}

12.2. ArticleService

package com.example.demo.service;import com.example.demo.dao.ArticleMapper;
import com.example.demo.model.Articleinfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;@Service
public class ArticleService {@Autowiredprivate ArticleMapper articleMapper;// 根据用户 id 查询此用户发表的所有文章public List<Articleinfo> getListByUid(int uid) {return articleMapper.getListByUid(uid);}// 判断文章的归属人+删除文章操作public int del(Integer aid, int uid) {return articleMapper.del(aid, uid);}// 添加文章到数据库public int add(Articleinfo articleinfo) {return articleMapper.add(articleinfo);}// 修改文章中间步骤: 查询自己发表的文章详情public Articleinfo getArticleByIdAndUid(int aid, int uid) {return articleMapper.getArticleByIdAndUid(aid, uid);}// 修改文章, 并效验归属人public int update(Articleinfo articleinfo) {return articleMapper.update(articleinfo);}// 根据文章 id 查询文章对象public Articleinfo getDetail(int aid) {return articleMapper.getDetailById(aid);}// 根据 uid 查询用户发表的总文章数public int getArtCountByUid(int uid) {return articleMapper.getArtCountByUid(uid);}// 更新文章阅读量public int incrementRCount(int aid) {return articleMapper.incrementRCount(aid);}// 查询一页的文章列表public List<Articleinfo> getListByPage(int psize, int offset) {return articleMapper.getListByPage(psize, offset);}// 查询文章表记录数public int getCount() {return articleMapper.getCount();}}

13. 核心—控制层controller

控制层是最为核心的一层,负责各种业务逻辑的处理。

package com.example.demo.controller;import com.example.demo.common.AppVariable;
import com.example.demo.common.PasswordUtils;
import com.example.demo.common.ResultAjax;
import com.example.demo.model.Userinfo;
import com.example.demo.model.vo.UserinfoVO;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;// ...
}
package com.example.demo.controller;import com.example.demo.common.ResultAjax;
import com.example.demo.common.SessionUtils;
import com.example.demo.model.Articleinfo;
import com.example.demo.model.Userinfo;
import com.example.demo.model.vo.UserinfoVO;
import com.example.demo.service.ArticleService;
import com.example.demo.service.UserService;
import org.apache.ibatis.annotations.Insert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;@RestController
@RequestMapping("/art")
public class ArticleController {@Autowiredprivate ArticleService articleService;private static final int _DESC_LENGTH = 120; // 文章简介的长度@Autowiredprivate ThreadPoolTaskExecutor taskExecutor;@Autowiredprivate UserService userService;// ...
}

13.1. UserController

13.1.1. 注册功能

♨️前后端交互接口

后端:

  1. /user/reg
  2. -1 非法参数 | 返回受影响行数

前端:

  1. post,json,/user/reg
  2. data:username,password

♨️后端实现

  1. 前端在经过一系列的校验之后会给传过来一组 json 数据。
  2. 我们用 UserinfoVO 接收然后将密码进行加密处理。
  3. 调用数据库然后将数据插入其中。
 /*** 注册功能接口*/
@RequestMapping("/reg")
public ResultAjax reg(UserinfoVO userinfo) {// 1. 效验参数if (userinfo == null || !StringUtils.hasLength(userinfo.getUsername())|| !StringUtils.hasLength(userinfo.getPassword())) {// 参数异常return ResultAjax.fail(-1, "非法参数");}// 密码加盐userinfo.setPassword(PasswordUtils.encrypt(userinfo.getPassword()));// 2. 请求 service 进行添加操作int result = userService.reg(userinfo);// 3. 将执行的结果返回给前端return ResultAjax.succ(result);
}

♨️涉及到的 sql 接口

// 将注册用户及密码保存到数据库中
@Insert("insert into userinfo(username,password) values(#{username},#{password})")
int reg(Userinfo userinfo);

♨️拦截器配置

不需要拦截,需要放开 URL。

.excludePathPatterns("/user/reg")

♨️前端逻辑及事件

  1. 用户名不能全为空,并且上传时空白符会被去除掉。
  2. 密码不能全为空,并且上传时空白符会被去除掉。
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>注册页面</title><link rel="stylesheet" href="css/conmmon.css"><link rel="stylesheet" href="css/reg.css"><script src="js/jquery.min.js"></script>
</head><body>
<!-- 导航栏 -->
<div class="nav"><img src="img/logo2.jpg" alt=""><span class="title">博客系统</span><!-- 用来占据中间位置 --><span class="spacer"></span><a href="blog_list.html">博客广场</a><a href="login.html">登录</a><!-- <a href="#">注销</a> -->
</div>
<!-- 版心 -->
<div class="register-container"><!-- 中间的注册框 --><div class="register-dialog"><h3>注册</h3><div class="row"><span>用户名</span><input type="text" id="username"></div><div class="row"><span>密码</span><input type="password" id="password"></div><div class="row"><span>确认密码</span><input type="password" id="password2"></div><div class="row"><button id="submit" onclick="mysub()">注册</button></div><a href="login.html" class="login">已有账户?登录</a></div>
</div>
<script>// 提交用户注册信息function mysub() {// 1. 参数效验 (获取到数据和非空效验)var username = jQuery("#username");var password = jQuery("#password");var password2 = jQuery("#password2");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 (password.val() != password2.val()) {alert("两次密码不一致, 请先检查! ");return false;}// 2. 将数据提交给后端jQuery.ajax({url: "/user/reg",type: "POST",data: {"username": username.val().trim(),"password": password.val().trim()},success: function (res) {// 3. 将后端返回的结果展示给用户if (res.code == 200 && res.data == 1) {// 注册成功alert("注册成功, 欢迎加入!");// 调转到登录页location.href = "login.html";} else {// 注册失败alert("出错了: 注册失败, 请重新操作! " + res.msg);}}});}
</script>
</body></html>

13.1.2. 登录功能

♨️前后端交互接口

后端:

  1. /user/login
  2. -1 非法参数 | -2 用户名或密码错误 | 1

前端:

  1. GET,json,/user/login
  2. data:username,password

♨️后端实现

  1. 首先进行非空校验,判断用户名和密码是否为空。
  2. 使用用户名进行查询,看当前用户信息是否存在,存在拿到加密密码及 UUID
  3. 把拿到的用户信息中的加密密码与待验证密码进行对比。
  4. 验证成功将用户对象存储到 Session 中。
/*** 登录功能接口*/
@RequestMapping("/login")
public ResultAjax login(UserinfoVO userinfoVO, HttpServletRequest request) {// 1. 参数效验if (userinfoVO == null || !StringUtils.hasLength(userinfoVO.getUsername()) ||!StringUtils.hasLength(userinfoVO.getPassword())) {// 非法登录return ResultAjax.fail(-1, "非法参数!");}// 2. 根据用户名查询对象Userinfo userinfo = userService.getUserByName(userinfoVO.getUsername());if (userinfo == null || userinfo.getId() == 0) {// 不存在此用户return ResultAjax.fail(-2, "用户名或密码错误!");}// 3. 使用对象中的密码和用户输入的密码进行比较// 加盐解密if (!PasswordUtils.decrypt(userinfoVO.getPassword(), userinfo.getPassword())) {// 密码错误return ResultAjax.fail(-2, "用户名或密码错误!");}// 4. 比较成功之后,将对象存储到 session 中HttpSession session = request.getSession();session.setAttribute(AppVariable.SESSION_USERINFO_KEY, userinfo);// 5. 将结果返回给用户return ResultAjax.succ(1);
}

♨️涉及到的 sql 接口

// 根据用户名查询用户对象
@Select("select * from userinfo where username=#{username}")
Userinfo getUserByName(@Param("username") String username);

♨️拦截器配置

不需要拦截,需要放开 URL。

.excludePathPatterns("/user/login")

♨️前端逻辑及事件

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>登录页面</title><link rel="stylesheet" href="css/conmmon.css"><link rel="stylesheet" href="css/login.css"><script src="js/jquery.min.js"></script>
</head><body>
<!-- 导航栏 -->
<div class="nav"><img src="img/logo2.jpg" alt=""><span class="title">博客系统</span><!-- 用来占据中间位置 --><span class="spacer"></span><a href="blog_list.html">博客广场</a><a href="reg.html">注册</a>
</div>
<!-- 版心 -->
<div class="login-container"><!-- 中间的登录框 --><div class="login-dialog"><h3>登录</h3><div class="row"><span>用户名</span><input type="text" id="username" placeholder="手机号/邮箱"></div><div class="row"><span>密码</span><input type="password" id="password"></div><div class="row"><button id="submit" onclick="doLogin()">登录</button></div><a href="reg.html">注册</a></div>
</div>
<script>// 执行登录操作function doLogin() {// 1. 效验参数// 拿到用户和密码两个框的组件var username = jQuery("#username");var password = jQuery("#password");if (username.val().trim() == "") {alert("请先输入用户名! ");username.focus();return false;}if (password.val().trim() == "") {alert("请先输入密码! ");password.focus();return false;}// 2. 将数据提交给后端jQuery.ajax({url: "/user/login",type: "GET",data: {"username": username.val(),"password": password.val()},success: function (res) {// 3. 将结果展示给用户if (res.code == 200 && res.data == 1) {// 登录成功// alert("恭喜: 登录成功!");// 跳转到我的文章管理页面location.href = "myblog_list.html";} else {// 登录失败alert("出错了: 登录失败, 请重新操作! " + res.msg);}}});}
</script>
</body></html>

13.1.3. 注销功能

♨️前后端交互接口

后端:

  1. /user/logout
  2. 1

前端:

  1. POST,/user/logout

♨️后端实现

直接将用户 Session 给删除即可。

//注销功能
@RequestMapping("/logout")
public AjaxResult logout(HttpSession session) {session.removeAttribute(AppVariable.USER_SESSION_KEY);return AjaxResult.success(1);
}

♨️拦截器配置:需要拦截。

♨️前端逻辑及事件

在个人列表,汇总列表,博客详情页,博客编辑页的导航栏都有该事件的触发按钮。

img

// 注销功能 js
function logout() {if (confirm("是否确定注销? ")) {// 1. 去后端删除 session 信息jQuery.ajax({url: "/user/logout",type: "POST",data: {},success: function (res) {}});// 2. 跳转到登录页面location.href = "login.html";}
}

13.1.4. 判断当前用户是否登录

♨️前后端交互接口

后端:

  1. /user/isLogin
  2. true/false

前端:

  1. GET,/user/isLogin

♨️后端实现

判断用户 Session 是否存在即可。

/*** 判断用户当前是否登录, 用来修改前端组建*/
@RequestMapping("/isLogin")
public ResultAjax isLogin(HttpServletRequest request) {HttpSession session = request.getSession(false);if (session != null && session.getAttribute(AppVariable.SESSION_USERINFO_KEY) != null) {return ResultAjax.succ(true);}return ResultAjax.succ(false);
}

♨️拦截器配置

不需要拦截,需要放开 URL。

.excludePathPatterns("/user/isLogin")

♨️前端逻辑及事件

后端返回 true,就将导航栏的 “登录” 按钮改为 “注销”。

在汇总列表页和博客详情页登录状态会触发该事件,整合在两个页面初始化的过程中。

img

// 判断用户是否登录,登录状态将 "登录" 按钮改为注销
jQuery.ajax({url: "/user/isLogin",type: "GET",data: {},success: function (res) {// 3. 将结果展示给用户if (res.code == 200 && res.data == true) {// 获取要修改的 <a> 元素var linkElement = document.getElementById("logoutLink");// 修改 href 属性为 "javascript:logout()"linkElement.href = "javascript:logout()";// 修改文本内容为 "注销"linkElement.textContent = "注销";}}
});

13.2. ArticleController

13.2.1. 返回当前登录用户的文章列表

♨️前后端交互接口

后端:

  1. /art/mylist
  2. -1 | 返回博客列表信息

前端:

  1. GET,/art/mylist

♨️后端实现

  1. 通过 Session 获取到当前登录用户的 id,根据用户 id 去查询当前用户的所有文章。
  2. 对文章正文部分进行截取,得到摘要。
/*** 得到当前登录用户的文章列表*/
@RequestMapping("/mylist")
public ResultAjax myList(HttpServletRequest request) {// 1. 得到当前登录用户Userinfo userinfo = SessionUtils.getUser(request);if (userinfo == null) {return ResultAjax.fail(-1, "当前未登录: 请先注册/登录! ");}// 2. 根据用户 id 查询此用户发表的所有文章List<Articleinfo> list = articleService.getListByUid(userinfo.getId());// 处理 list -> 将文章正文变成简介if (list != null && list.size() > 0) {// 并行处理 list 集合list.stream().parallel().forEach((art) -> {if (art.getContent().length() > _DESC_LENGTH) {// 截取art.setContent(art.getContent().substring(0, _DESC_LENGTH));}});}// 3. 返回给前端return ResultAjax.succ(list);
}

♨️涉及到的 sql 接口

// 根据用户 id 查询此用户发表的所有文章
@Select("select * from articleinfo where uid=#{uid} order by id desc")
List<Articleinfo> getListByUid(@Param("uid") int uid);

♨️拦截器配置:需要拦截。

♨️前端逻辑及事件

img

// 初始化方法
function init() {jQuery.ajax({url: "/art/mylist",type: "GET",data: {},success: function (res) {if (res.code == 200) {// 请求成功var createHtml = "";var artList = res.data;if (artList == null || artList.length == 0) {// 未发表文章createHtml += "<h3 style='margin-left:20px;margin-top:20px'>暂无文章, 快去" +"<a href='blog_add.html'>创作</a>吧! </h3>";} else {for (var i = 0; i < artList.length; i++) {var art = artList[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.content;createHtml += '</div>';createHtml += ' <a href="blog_content.html?aid=' +art.id + '" class="detail">查看全文</a>&nbsp;&nbsp;';createHtml += '<a href="blog_edit.html?aid=' +art.id + '" class="detail">修改</a>&nbsp;&nbsp;';createHtml += ' <a href="javascript:del(' + art.id + ')" class="detail">删除</a>';createHtml += '</div>';}}jQuery("#artListDiv").html(createHtml);} else {alert("" + res.msg);}}});
}

13.2.2. 删除文章功能

♨️前后端交互接口

后端:

  1. /art/del
  2. -1 | 受影响行数

前端:

  1. POST,json,/art/delete
  2. data:aid(文章id)

♨️后端实现

删除时需要两个参数,一个是文章的 id 一个是当前登录用户的 id,当登录用户 id 文章所属用户 id 要相同才能删除文章。

/*** 查询文章详情页*/
@RequestMapping("/detail")
public ResultAjax detail(Integer aid) throws ExecutionException, InterruptedException {// 1. 参数效验if (aid == null || aid <= 0) {return ResultAjax.fail(-1, "非法参数! ");}// 2. 查询文章详情Articleinfo articleinfo = articleService.getDetail(aid);if (articleinfo == null || articleinfo.getId() <= 0) {return ResultAjax.fail(-1, "非法参数! ");}// 3 和 4 是多线程同步查询// 3. 根据 uid 查询用户的详情FutureTask<UserinfoVO> userTask = new FutureTask(() -> {return userService.getUserById(articleinfo.getUid());});taskExecutor.submit(userTask);// 4. 根据 uid 查询用户发表的总文章数FutureTask<Integer> artCountTask = new FutureTask<>(() -> {return articleService.getArtCountByUid(articleinfo.getUid());});taskExecutor.submit(artCountTask);// 5. 组装数据UserinfoVO userinfoVO = userTask.get(); // 等待任务 (线程池) 执行完成int artCount = artCountTask.get(); // 等待任务 (线程池) 执行完成userinfoVO.setArtCount(artCount);HashMap<String, Object> result = new HashMap<>();result.put("user", userinfoVO);result.put("art", articleinfo);// 6. 返回结果给前端return ResultAjax.succ(result);
}

♨️涉及到的 sql 接口

// 判断文章的归属人+删除文章操作
@Delete("delete from articleinfo where id=#{aid} and uid=#{uid}")
int del(@Param("aid") Integer aid, int uid);

♨️拦截器配置:需要拦截。

♨️前端逻辑及事件

img

// 根据文章 id 进行删除操作
function del(aid) {// 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);}}});
}

13.2.3. 查看文章详情功能

♨️前后端交互接口

后端:

  1. /art/detail
  2. count(文章数)& user(用户) & art(文章)

前端:

  1. GET,json,/art/detail
  2. data:aid

♨️后端实现

  1. 根据文章 id 查询文章信息,看文章是否存在,文章存在,执行后续步骤。
  2. 注册根据 uid 查询用户总文章数的任务
  3. 注册根据 uid 查询用户信息的任务
  4. 线程池执行任务
  5. 构造响应数据,并返回
/*** 查询文章详情页*/
@RequestMapping("/detail")
public ResultAjax detail(Integer aid) throws ExecutionException, InterruptedException {// 1. 参数效验if (aid == null || aid <= 0) {return ResultAjax.fail(-1, "非法参数! ");}// 2. 查询文章详情Articleinfo articleinfo = articleService.getDetail(aid);if (articleinfo == null || articleinfo.getId() <= 0) {return ResultAjax.fail(-1, "非法参数! ");}// 3 和 4 是多线程同步查询// 3. 根据 uid 查询用户的详情FutureTask<UserinfoVO> userTask = new FutureTask(() -> {return userService.getUserById(articleinfo.getUid());});taskExecutor.submit(userTask);// 4. 根据 uid 查询用户发表的总文章数FutureTask<Integer> artCountTask = new FutureTask<>(() -> {return articleService.getArtCountByUid(articleinfo.getUid());});taskExecutor.submit(artCountTask);// 5. 组装数据UserinfoVO userinfoVO = userTask.get(); // 等待任务 (线程池) 执行完成int artCount = artCountTask.get(); // 等待任务 (线程池) 执行完成userinfoVO.setArtCount(artCount);HashMap<String, Object> result = new HashMap<>();result.put("user", userinfoVO);result.put("art", articleinfo);// 6. 返回结果给前端return ResultAjax.succ(result);
}

♨️涉及到的 sql 接口

// 根据文章 id 查询文章对象
@Select("select * from articleinfo where id=#{aid}")
Articleinfo getDetailById(@Param("aid") int aid);
// 根据用户 id 查询用户对象
@Select("select * from userinfo where id=#{uid}")
UserinfoVO getUserById(@Param("uid") int uid);
// 根据 uid 查询用户发表的总文章数
@Select("select count(*) from articleinfo where uid=#{uid}")
int getArtCountByUid(@Param("uid") int uid);

♨️拦截器配置

  1. 该接口不需要拦截,需要放开 URL。
  2. editor.md 是个目录,要放开整个目录才行,不然页面渲染就出问题了,其他地方不加能渲染是因为是登录状态,但详情页是不需要登录的。
.excludePathPatterns("/art/detail")
.excludePathPatterns("/editor.md/**")

♨️前端逻辑及事件

img

// 获取查询字符串参数值: 根据 key 获取 url 中对应的 value
function getParamValue(key) {// 1. 得到当前url的参数部分var params = location.search;// 2. 去除“?”if (params.indexOf("?") >= 0) {params = params.substring(1);// 3. 根据“&”将参数分割成多个数组var paramArray = params.split("&");// 4. 循环对比 key, 并返回查询的 valueif (paramArray.length >= 1) {for (var i = 0; i < paramArray.length; i++) {// key=valuevar item = paramArray[i].split("=");if (item[0] == key) {return item[1];}}}}return null;
}
<script type="text/javascript">var aid = getParamValue("aid");var editormd;function initEdit(md) {editormd = editormd.markdownToHTML("editorDiv", {markdown: md, // Also, you can dynamic set Markdown text// htmlDecode : true,  // Enable / disable HTML tag encode.// htmlDecode : "style,script,iframe",  // Note: If enabled, you should filter some dangerous HTML tags for website security.});}// 初始化页面function init() {// 1. 效验参数if (aid == null || aid <= 0) {alert("参数有误! ");return false;}// 2. 请求后端获取数据jQuery.ajax({url: "/art/detail",type: "GET",data: {"aid": aid},success: function (res) {// 3. 将数据展示到前端if (res.code == 200 && res.data != null) {var user = res.data.user;var art = res.data.art;if (user != null) {// 给用户对象设置值if (user.photo != "") {jQuery("#photo").att("src", user.photo);}jQuery("#username").html(user.username);jQuery("#artcount").html(user.artCount); // 用户发布的总文章数} else {alert("出错了: 查询失败,   请重新操作! " + res.msg);}if (art != null) {jQuery("#title").html(art.title);jQuery("#createtime").html(art.createtime);jQuery("#rcount").html(art.rcount); // 阅读量initEdit(art.content);} else {alert("出错了: 查询失败,   请重新操作! " + res.msg);}} else {alert("出错了: 查询失败,   请重新操作! " + res.msg);}}});init();}
}

13.2.4. 更新文章阅读量

♨️前后端接口

后端:

  1. /art/increment_rcount
  2. -1 | 返回受影响行数

前端:

  1. POST,json,/art/increment_rcount
  2. data:aid
*** 更新文章阅读量*/
@RequestMapping("/increment_rcount")
public ResultAjax incrementRCount(Integer aid) {// 1. 效验参数if (aid == null || aid <= 0) {return ResultAjax.fail(-1, "参数有误! ");}// 2. 更新数据库 update articleinfo set rcount=rcount+1 where aid=#{aid}int result = articleService.incrementRCount(aid);// 3. 返回结果return ResultAjax.succ(result);
}

♨️涉及到的 sql 接口

// 更新文章阅读量
@Update("update articleinfo set rcount=rcount+1 where id=#{aid}")
int incrementRCount(@Param("aid") int aid);

♨️拦截器配置

不需要拦截,需要放开 URL。

.excludePathPatterns("/art/getlistbypage")

♨️前端逻辑及事件

在查看文章详情页初始化模块中整合。

// 访问量加 1
function incrementRCount() {if (aid == null || aid <= 0) {return false;}jQuery.ajax({url: "/art/increment_rcount",type: "POST",data: {"aid": aid},success: function (res) {}});
}incrementRCount();

13.2.5. 添加文章

♨️前后端接口

后端:

  1. /art/add
  2. 返回受影响行数

前端:

  1. POST,json,/art/add
  2. data:title,content

♨️后端实现

  1. 通过 Session 得到当前登录用户的 id。
  2. 将用户 id 赋值到文章对象后插入到数据库。
/*** 添加文章*/
@RequestMapping("/add")
public ResultAjax add(Articleinfo articleinfo, HttpServletRequest request) {// 1. 效验参数if (articleinfo == null || !StringUtils.hasLength(articleinfo.getTitle()) ||!StringUtils.hasLength(articleinfo.getContent())) {return ResultAjax.fail(-1, "非法参数! ");}// 2. 组装数据Userinfo userinfo = SessionUtils.getUser(request);if (userinfo == null) {return ResultAjax.fail(-2, "当前未登录: 请先注册/登录! ");}articleinfo.setUid(userinfo.getId());// 3. 将数据入库int result = articleService.add(articleinfo);// 4. 将结果返回给前端return ResultAjax.succ(result);
}

♨️涉及到的 sql 接口

// 添加文章到数据库
@Insert("insert into articleinfo(title,content,uid) values(#{title},#{content},#{uid})")
int add(Articleinfo articleinfo);

♨️拦截器配置:需要拦截。

♨️前端逻辑及事件

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>博客添加</title><!-- 引入自己写的样式 --><link rel="stylesheet" href="css/conmmon.css"><link rel="stylesheet" href="css/blog_edit.css"><!-- 引入 editor.md 的依赖 --><link rel="stylesheet" href="editor.md/css/editormd.min.css"/><script src="js/jquery.min.js"></script><script src="editor.md/editormd.js"></script><script src="js/logout.js"></script>
</head><body>
<!-- 导航栏 -->
<div class="nav"><img src="img/logo2.jpg" alt=""><span class="title">博客系统</span><!-- 用来占据中间位置 --><span class="spacer"></span><a href="blog_list.html">博客广场</a><a href="javascript:logout()">注销</a>
</div>
<!-- 编辑框容器 -->
<div class="blog-edit-container"><!-- 标题编辑区 --><div class="title"><input id="title" type="text" placeholder="在这里写下文章标题"><button onclick="mysub()">发布文章</button></div><!-- 创建编辑器标签 --><div id="editorDiv"><textarea id="editor-markdown" style="display:none;"></textarea></div>
</div><script>var editor;function initEdit(md) {// 编辑器设置editor = editormd("editorDiv", {// 这里的尺寸必须在这里设置. 设置样式会被 editormd 自动覆盖掉.width: "100%",// 高度 100% 意思是和父元素一样高. 要在父元素的基础上去掉标题编辑区的高度height: "calc(100% - 50px)",// 编辑器中的初始内容markdown: md,// 指定 editor.md 依赖的插件路径path: "editor.md/lib/",saveHTMLToTextarea: true //});}initEdit("# 在这里写下一篇博客"); // 初始化编译器的值// 提交function mysub() {// 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(),"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);}}});}
</script>
</body></html>

13.2.6. 修改文章

13.2.6.1. 页面初始化

♨️前后端交互接口

后端:

  1. /art/update_init
  2. -1 | 文章信息

前端:

  1. GET,json,/art/update_init
  2. data:aid

♨️后端实现

/*** 修改文章中间步骤: 查询自己发表的文章详情*/
@RequestMapping("/update_init")
public ResultAjax updateInit(Integer aid, HttpServletRequest request) {// 1. 参数效验if (aid == null || aid <= 0) {return ResultAjax.fail(-1, "参数有误!");}// 2. 得到当前登录用户 idUserinfo userinfo = SessionUtils.getUser(request);if (userinfo == null) {return ResultAjax.fail(-2, "当前未登录: 请先注册/登录! ");}// 3. 查询文章并效验权限 where id=#{aid} and uid=#{uid}Articleinfo articleinfo = articleService.getArticleByIdAndUid(aid, userinfo.getId());// 4. 将结果返回给前端return ResultAjax.succ(articleinfo);
}

♨️涉及到的 sql 接口

// 修改文章中间步骤: 查询自己发表的文章详情
@Select("select * from articleinfo where id=#{aid} and uid=#{uid}")
Articleinfo getArticleByIdAndUid(@Param("aid") int aid, @Param("uid") int uid);

♨️拦截器配置:需要拦截。

♨️前端逻辑及事件

<script>var aid = getParamValue("aid"); // 文章id// 初始化页面的方法function init() {// 1. 效验 aidif (aid == null || aid <= 0) {alert("非法参数!");return false;}// 2. 查询文章详情jQuery.ajax({url: "/art/update_init",type: "GET",data: {"aid": aid},success: function (res) {// 3. 将文章的详情信息展示到页面if (res.code == 200 && res.data != null && res.data.id > 0) {// 查询到了文章信息jQuery("#title").val(res.data.title);initEdit(res.data.content);} else if (res.code == -2) {alert("出错了: 操作失败, 请重新操作! " + res.msg);location.href = "login.html";} else {alert("出错了: 操作失败, 请重新操作! " + res.msg);}}});}init();
}
13.2.6.2. 发布修改后的文章

♨️前后端交互接口
后端:

  1. /art/update
  2. 返回受影响行数

前端:

  1. POST,json,/art/update
  2. data:id(文章id),title,content

♨️后端实现

/*** 修改文章信息*/
@RequestMapping("/update")
public ResultAjax update(Articleinfo articleinfo, HttpServletRequest request) {// 1. 参数效验if (articleinfo == null ||!StringUtils.hasLength(articleinfo.getTitle()) ||!StringUtils.hasLength(articleinfo.getContent()) ||articleinfo.getId() == 0) {return ResultAjax.fail(-1, "非法参数!");}// 2. 获取登录用户Userinfo userinfo = SessionUtils.getUser(request);if (userinfo == null) {return ResultAjax.fail(-2, "当前未登录: 请先注册/登录! ");}articleinfo.setUid(userinfo.getId());// 3. 修改文章, 并效验归属人int result = articleService.update(articleinfo);// 4. 返回结果return ResultAjax.succ(result);
}

♨️涉及到的 sql 接口

 // 修改文章, 并效验归属人
@Update("update articleinfo set title=#{title},content=#{content} where id=#{id} and uid=#{uid}")
int update(Articleinfo articleinfo);

♨️拦截器配置:需要拦截。

♨️前端逻辑及事件

img

// 执行修改操作
function doUpdate() {// 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/update",type: "POST",data: {"id": aid,"title": title.val(),"content": editor.getValue()},success: function (res) {if (res.code == 200 && res.data == 1) {// 修改成功alert("文章修改成功! ");// 跳转到我的文章管理员location.href = "myblog_list.html";} else if (res.code == -2) {alert("当前未登录, 请在登录后操作! ");location.href = "login.html";} else {alert("出错了: 修改失败, 请重新操作! " + res.msg);}}});// 3. 将后端返回的结果展现给用户
}

13.2.7. 根据分页来查询汇总列表

♨️前后端交互接口

后端:

  1. /art/getlistbypage
  2. size(最大页码),list(一页的博客列表信息)

前端:

  1. GET,/art/getlistbypage
  2. data:pindex(页码)& psize(页内最大博客数)

♨️后端实现

后端只要知道页码和一页多少条数据,就可以计算出要选取哪几条数据然后返回。

psize:一页几条数据,就是limit后面的值

pindex:根据这个值可以计算储偏移量为psize × (pindex - 1),就是offset后面的值。

所有博客数 / psize,向上取整就是最大页码size,如果所有博客数为 0,则前端应该显示,当前在第 0 页,共 0 页。

pindex正常操作下是不会出错的,因为前端知道最大页码size,会做出判断。

 /*** 查询所有博客: 分页查询*/@RequestMapping("/getlistbypage")public ResultAjax getListByPage(Integer pindex, Integer psize) throws ExecutionException, InterruptedException {// 1. 参数矫正if (pindex == null || pindex < 1) {pindex = 1; // 参数矫正}if (psize == null || psize < 1) {psize = 2; // 参数矫正}// 2. 并发进行文章列表和总页数的查询// 2.1 查询分页列表数据int finalOffset = psize * (pindex - 1); // 分页公式 (偏移位置, 即从第几条数据开始查)int finalPSize = psize;FutureTask<List<Articleinfo>> listTask = new FutureTask<>(() -> {List<Articleinfo> list = articleService.getListByPage(finalPSize, finalOffset);if (list != null && list.size() > 0) {// 并行处理 list 集合list.stream().parallel().forEach((art) -> {if (art.getContent().length() > _DESC_LENGTH) {// 截取art.setContent(art.getContent().substring(0, _DESC_LENGTH));}});}return list;});// 2.2 查找总页数FutureTask<Integer> sizeTask = new FutureTask<>(() -> {// 总条数int totalCount = articleService.getCount();double sizeTemp = (totalCount * 1.0) / finalPSize;// 向上取整return (int) Math.ceil(sizeTemp);});taskExecutor.submit(listTask);taskExecutor.submit(sizeTask);// 3. 组装数据List<Articleinfo> list = listTask.get();int size = sizeTask.get();HashMap<String, Object> map = new HashMap<>();map.put("list", list);map.put("size", size);// 4. 将结果返回给前端return ResultAjax.succ(map);}

♨️涉及到的 sql 接口

// 查询一页的文章列表
@Select("select * from articleinfo order by id desc limit #{psize} offset #{offset}")
public List<Articleinfo> getListByPage(@Param("psize") int psize, @Param("offset") int offset);
// 查询文章表记录数
@Select("select count(*) from articleinfo")
int getCount();

♨️拦截器配置

不需要拦截,需要放开 URL。

.excludePathPatterns("/art/getlistbypage")

♨️前端逻辑及事件

img

<script>var psize = 2; // 每页显示条数var pindex = 1; // 页码var totalpage = 1; // 总共有多少页// 初始化数据function init() {// 1. 处理分页参数psize = getParamValue("psize");if (psize == null) {psize = 2; // 每页显示条数}pindex = getParamValue("pindex");if (pindex == null) {pindex = 1; // 页码}jQuery("#pindex").html(pindex);// 2. 请求后端接口jQuery.ajax({url: "/art/getlistbypage",type: "GET",data: {"pindex": pindex,"psize": psize},success: function (res) {// 3. 将结果展示给用户if (res.code == 200 && res.data != null) {var createHtml = "";if (res.data.list != null && res.data.list.length > 0) {// 有文章totalpage = res.data.size;jQuery("#pszie").html(totalpage);var artlist = res.data.list;for (var i = 0; i < artlist.length; i++) {var art = artlist[i]; // 文章对象createHtml += '<div class="blog" >';createHtml += '<div class="title">' + art.title + '</div>';createHtml += '<div class="date">' + art.createtime + '</div>';createHtml += '<div class="desc">' + art.content + '</div>';createHtml += '<a href="blog_content.html?aid=' +art.id + '" class="detail">查看全文</a>';createHtml += '</div>';}} else {// 暂无文章createHtml += '<h3 style="margin-top:20px;margin-left:20px;">不好意思, 暂无文章! </h3>';}jQuery("#artListDiv").html(createHtml);} else {alert("出错了: 查询失败,  请重新操作! " + res.msg);}}});}init();// 点击首页function doFirst() {// 1. 判断是否在首页if (pindex <= 1) {alert("当前已经是第一页了哦! ");return false;}// 2. 跳转到首页location.href = "blog_list.html";}// 点击末页function doLast() {// 1. 判断是否在末页if (pindex >= totalpage) {alert("当前已经是最后一页了哦! ");return false;}// 2. 跳转到末页location.href = "blog_list.html?pindex=" + totalpage;}// 点击 "上一页"function doBefore() {// 1. 判断是否在首页if (pindex <= 1) {alert("当前已经是第一页了哦! ");return false;}// 2. 跳转上一页location.href = "blog_list.html?pindex=" + (parseInt(pindex) - 1);}// 点击 "下一页"function doNext() {// 1. 判断是否在末页if (pindex >= totalpage) {alert("当前已经是最后一页了哦! ");return false;}// 2. 跳转到下一页location.href = "blog_list.html?pindex=" + (parseInt(pindex) + 1);}
</script>

14. Session升级存储到Redis

添加依赖:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId>
</dependency>

添加redis配置信息(properties):

# redis 配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.database=2
spring.session.store-type=redis

15. 项目部署

1️⃣Linux 中安装 Redis

使用以下命令,直接将 redis 安装到 linux 服务器:

yum -y install redis

2️⃣设置 Redis 远程连接

这一步需要修改 Redis 中的一些配置

  1. 进入 redis 配置文件的编写模式,redis 配置文件就是 linux 下的 /etc/redis.conf。
  2. 将 redis.conf 中的 “bind 127.0.0.1” 注释掉。
  3. 将 redis.conf 中的 “protected-mode yes” 改为 “protected-mode no”。
  4. 保存并退出。
  5. 使用命令 “redis-cli shutdown” 先关闭 redis 服务,再使用 “redis-server /etc/redis.conf &” 启动 redis 服务。
  6. redis 在服务器的端口默认是 6379,配置防火墙或者安全组将这个端口开放。

3️⃣启动 Redis

使用以下命令,以后台运行方式启动 redis:

redis-server /etc/redis.conf &

4️⃣打包上传项目

将程序打包为.jar包上传到云服务器。

要注意,在打包项目的时候,⼀定要检查,确保数据库连接的是远程服务器的 MySQL,确保密码正确;确保 Rdeis 端口配置正确。

5️⃣启动项目

使⽤以下命令启动 Spring Boot 项⽬并后台运行:

nohup java -jar xxx.jar &

6️⃣停止项目

停止 Spring Boot 项目需要两步:

  1. 查询出运行的 Spring Boot 的进程,使用命令:
ps -ef | grep java
  1. 将 Spring Boot 的进程结束掉,使用命令:
kill -9 进程ID

16. 项目亮点

  1. 应用到了多线程提高业务处理效率。
  2. 列表显示实现了一个分页功能。
  3. 密码的存储使用了自己写加盐加密算法。
  4. 用到了AOP编程,统一处理与拦截器。
  5. Session 存储到 Redis,可以让多个服务器共享 Session 数据。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/140936.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

nodejs express vue uniapp电影购票系统源码

开发技术&#xff1a; node.js&#xff0c;vscode&#xff0c;HBuilder X express vue elementui uniapp 功能介绍&#xff1a; 用户端&#xff1a; 登录注册 首页显示搜索电影&#xff0c;轮播图&#xff0c;电影分类&#xff0c;最近上架电影 点击电影进入电影详情&am…

数据结构线性表——栈

前言&#xff1a;哈喽小伙伴们&#xff0c;今天我们将一起进入数据结构线性表的第四篇章——栈的讲解&#xff0c;栈还是比较简单的哦&#xff0c;跟紧博主的思路&#xff0c;不要掉队哦。 目录 一.什么是栈 二.如何实现栈 三.栈的实现 栈的初始化 四.栈的操作 1.数据入栈…

Apache Druid连接回收引发的血案

问题 线上执行大批量定时任务&#xff0c;发现SQL执行失败的报错&#xff1a; CommunicationsException, druid version 1.1.10, jdbcUrl : jdbc:mysql://xxx?useUnicodetrue&characterEncodingUTF-8&zeroDateTimeBehaviorconvertToNull,testWhileIdle true, idle …

Zigbee智能家居方案设计

背景 目前智能家居物联网中最流行的三种通信协议&#xff0c;Zigbee、WiFi以及BLE&#xff08;蓝牙&#xff09;。这三种协议各有各的优势和劣势。本方案基于CC2530芯片来设计&#xff0c;CC2530是TI的Zigbee芯片。 网关使用了ESP8266CC2530。 硬件实物 节点板子上带有继电器…

Hosts File Editor 实用工具

我一般手工编辑hosts文件&#xff0c;我想给hosts文件加一个开关&#xff0c;本想自己实现&#xff0c;但是忽然发现微软已经提供了官方的解决方案&#xff0c;感觉有能人。 对文件的行的修改被抽象成了一个开关。腻害&#xff01;&#xff01;&#xff01;

◢Django 自写分页与使用

目录 1、设置分页样式,并展示到浏览器 2、模拟页码 3、生成分页 4、数据显示 5、上一页下一页 6、数据库的数据分页 7、封装分页 8、使用封装好的分页 建立好app后&#xff0c;设置路径path(in2/,views.in2)&#xff0c;视图def in2(request): &#xff0c;HTML: in2.html…

RK3568笔记五:基于Yolov5的训练及部署

若该文为原创文章&#xff0c;转载请注明原文出处。 一. 部署概述 环境&#xff1a;Ubuntu20.04、python3.8 芯片&#xff1a;RK3568 芯片系统&#xff1a;buildroot 开发板&#xff1a;ATK-DLRK3568 开发主要参考文档&#xff1a;《Rockchip_Quick_Start_RKNN_Toolkit2_C…

开源知识库软件xwiki在Windows下的安装

文章目录 开源知识库软件-xwiki在windows上的部署0、参考文档1、前置环境准备1.1、Windows版本及系统配置1.2、JDK11安装1.3、Tomcat9安装1.4、MySQL5.7数据库的安装 2、xwiki安装3、配置3.1、修改配置支持对文档内容进行搜索 4、问题解决4.1、附件无法上传问题4.1、附件无法下…

FreeRTOS知识梳理

一、RTOS:Real time operating system,中文意思为 实时操作系统&#xff0c;它是一类操作系统&#xff0c;比如uc/OS、FreeRTOS、RTX、RT-Thread 这些都是实时操作系统。 二、移植FreeRTOS到STM32F103C8T6上 interface选择CMSIS_V1,RCC选择Crystal Ceramic Resonator 。 …

海康威视(iVMS)综合安防系统任意文件上传漏洞复现 [附POC]

文章目录 海康威视&#xff08;iVMS&#xff09;综合安防系统任意文件上传漏洞复现 [附POC]0x01 前言0x02 漏洞描述0x03 影响版本0x04 漏洞环境0x05 漏洞复现1.访问漏洞环境2.构造POC3.复现 0x06 修复建议 海康威视&#xff08;iVMS&#xff09;综合安防系统任意文件上传漏洞复…

[PyTorch][chapter 62][强化学习-基本概念]

前言&#xff1a; 目录&#xff1a; 强化学习概念 马尔科夫决策 Bellman 方程 格子世界例子 一 强化学习 强化学习 必须在尝试之后&#xff0c;才能发现哪些行为会导致奖励的最大化。 当前的行为可能不仅仅会影响即时奖赏&#xff0c;还有影响下一步奖赏和所有奖赏 强…

使用Inis搭配内网穿透实现Ubuntu上快速搭建博客网站远程访问

文章目录 前言1. Inis博客网站搭建1.1. Inis博客网站下载和安装1.2 Inis博客网站测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar临时数据隧道2.2 Cpolar稳定隧道&#xff08;云端设置&#xff09;2.3.Cpolar稳定隧道&#xff08;本地设置&#xff09; 3. 公网访问测试总…

在 uniapp 中 一键转换单位 (px 转 rpx)

在 uniapp 中 一键转换单位 px 转 rpx Uni-app 官方转换位置利用【px2rpx】插件Ctrl S一键全部转换下载插件修改插件 Uni-app 官方转换位置 首先在App.vue中输入这个&#xff1a; uni.getSystemInfo({success(res) {console.log("屏幕宽度", res.screenWidth) //屏…

酷柚易汛ERP - 商品库存余额表操作指南

1、应用场景 商品库存余额表用于查询商品在各仓库的实际结存量、单位成本以及成本等明细。 2、主要操作 打开【仓库】-【商品库存余额表】&#xff0c;可筛选仓库、商品、商品类别&#xff0c;导出/打印等操作见【销货单】不再赘述。 3、分享操作 库存余额分享&#xff0c;…

CCLink转Modbus TCP网关_MODBUS网口设置

兴达易控CCLink转Modbus TCP网关是一种用于连接CCLink网络和Modbus TCP网络的设备。它提供了简单易用的MODBUS网口设置&#xff0c;可以帮助用户轻松地配置和管理网络连接 1 、网关做为MODBUS主站 &#xff08;1&#xff09;将电脑用网线连接至网关的P3网口上。 &#xff08;…

计算机网络(一)

一、什么是计算机网络、计算机协议&#xff1f; 计算机网络就是由计算机作为收发端&#xff0c;不同计算机相互连接的网络&#xff0c;包括互联网&#xff08;Internet&#xff09;&#xff0c;公司或者家用网络&#xff08;intranet&#xff09;等等&#xff1b;其中Internet…

【C语言 | 指针】C指针详解(经典,非常详细)

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…

【数据结构】非递归实现二叉树的前 + 中 + 后 + 层序遍历(听说面试会考?)

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前学习C和算法 ✈️专栏&#xff1a;数据结构 &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章对你有帮助的话 欢迎 评论&#x1f4ac; 点赞&…

利用角色roles上线wordpress项目

角色订制&#xff1a;roles ① 简介 对于以上所有的方式有个弊端就是无法实现复用假设在同时部署Web、db、ha 时或不同服务器组合不同的应用就需要写多个yml文件。很难实现灵活的调用。   roles 用于层次性、结构化地组织playbook。roles 能够根据层次型结构自动装载变量文…

CentOS7、CentOS8 如何修改ip信息(修改网络信息)(无图形界面)(亲测可用)

文章目录 CentOS 7方法一&#xff1a;使用 nmcli 命令方法二&#xff1a;编辑配置文件&#xff08;我的CentOS7是使用这种方法&#xff0c;亲测可用&#xff09; CentOS 8方法一&#xff1a;使用 nmcli 命令方法二&#xff1a;编辑配置文件 在 CentOS 系统中&#xff0c;如果你…