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.rhkb.cn/news/189577.html

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

相关文章

Linux 基本语句_10_进程

进程和程序的区别&#xff1a; 程序是一段静态的代码&#xff0c;是保存在非易失储存器上的制令和数据的有序集合&#xff0c;没有任何执行的概念&#xff1b;而进程是一个动态的概念&#xff0c;它是程序的一次执行过程&#xff0c;包括了动态创建、调度、执行和消亡的整个过程…

面向切面编程AOP

2023.11.12 本章学习spring另一大核心——AOP。AOP是一种编程技术&#xff0c;底层是使用动态代理来实现的。Spring的AOP使用的动态代理是&#xff1a;JDK动态代理 CGLIB动态代理技术。Spring在这两种动态代理中灵活切换&#xff0c;如果是代理接口&#xff0c;会默认使用JDK动…

KDE Plasma 6 将不支持较旧的桌面小部件

KDE Plasma 6 进行了一些修改&#xff0c;需要小部件作者进行调整。开发人员&#xff0c;移植时间到了&#xff01; KDE Plasma 6 是备受期待的桌面环境版本升级版本。 最近&#xff0c;其发布时间表公布&#xff0c;第一个 Alpha 版本将于 2023 年 11 月 8 日上线&#xff0…

【JVM系列】- 寻觅·方法区的内容

寻觅方法区的内容 &#x1f604;生命不息&#xff0c;写作不止 &#x1f525; 继续踏上学习之路&#xff0c;学之分享笔记 &#x1f44a; 总有一天我也能像各位大佬一样 &#x1f31d;分享学习心得&#xff0c;欢迎指正&#xff0c;大家一起学习成长&#xff01; 文章目录 寻觅…

一个java文件的JVM之旅

准备 我是小C同学编写得一个java文件&#xff0c;如何实现我的功能呢&#xff1f;需要去JVM(Java Virtual Machine)这个地方旅行。 变身 我高高兴兴的来到JVM&#xff0c;想要开始JVM之旅&#xff0c;它确说&#xff1a;“现在的我还不能进去&#xff0c;需要做一次转换&#x…

解析Python的深浅拷贝机制

引言 在Python编程中&#xff0c;我们经常会遇到数据复制的问题。有时候&#xff0c;我们只是需要复制一份数据的引用&#xff0c;有时候&#xff0c;我们则需要复制数据本身。这就涉及到了Python中的深浅拷贝问题。深浅拷贝是Python中的一个重要概念&#xff0c;理解它对于编…

微软和Red Hat合体:帮助企业更方便部署容器

早在2015年&#xff0c;微软就已经和Red Hat达成合作共同为企业市场开发基于云端的解决方案。时隔两年双方在企业市场的多个方面开展更紧密的合作&#xff0c;今天两家公司再次宣布帮助企业更方便地部署容器。 双方所开展的合作包括在微软Azure上部署Red Hat OpenShift&#xf…

Pyside6/PYQT6如何实现无边框设计,解决无边框窗口无法移动的问题

文章目录 💢 问题 💢💯 解决方案 💯🍔 准备工作🐾 操作步骤🐾 窗口无边框🐾 窗口透明🐾 实现窗口可移动⚓️ 相关链接 ⚓️💢 问题 💢 有时候我们需要一个无边框的UI设计来实现/美化一些功能,如:制作一个桌面时钟,进度条展示等,要实现无边框其实很简…

从开源项目聊鱼眼相机的“360全景拼接”

目录 概述 从360全景的背景讲起 跨过参数标定聊透视变化 拼接图片后处理 参考文献 概述 写这篇文章的原因完全源于开源项目(GitHub参阅参考文献1)。该项目涵盖了环视系统的较为全貌的制作过程&#xff0c;包含完整的标定、投影、拼接和实时运行流程。该篇文章主要是梳理全…

机器学习数据预处理——Word2Vec的使用

引言&#xff1a; Word2Vec 是一种强大的词向量表示方法&#xff0c;通常通过训练神经网络来学习词汇中的词语嵌入。它可以捕捉词语之间的语义关系&#xff0c;对于许多自然语言处理任务&#xff0c;包括情感分析&#xff0c;都表现出色。 代码&#xff1a; 重点代码&#…

科技改变农业:合成数据农业中的应用

介绍 农业在我们的生活中起着至关重要的作用&#xff0c;它为我们提供了生存的食物。如今&#xff0c;它遇到了各种困难&#xff0c;例如气候变化的影响、缺乏工人以及全球流行病造成的中断。这些困难影响了耕作用水和土地的供应&#xff0c;而这些水和土地正变得越来越稀缺。…

PROFINET和UDP、MODBUS-RTU通信速度对比实验

这篇博客我们介绍PROFINET 和MODBUS-RTU通信实验时的数据刷新速度,以及这种速度不同对控制系统带来的挑战都有哪些,在介绍这篇对比实验之前大家可以参考下面的文章链接: S7-1200PLC和SMART PLC的PN智能从站通信 S7-200 SMART 和 S7-1200PLC进行PROFINET IO通信-CSDN博客文…

Adobe Photoshop 2020给证件照换底

1.导入图片 2.用魔法棒点击图片 3.点选择&#xff0c;反选 4.选择&#xff0c;选择并遮住 5.用画笔修饰证件照边缘 6. 7.更换要换的底的颜色 8.新建图层 9.使用快捷键altdelete键填充颜色。 10.移动图层&#xff0c;完成换底。

计算机中丢失msvcr120.dll文件怎么修复?找不到msvcr120.dll五种完美修复方案

今天我想和大家分享的是关于“msvcr120.dll丢失的问题的5个解决方法”。在我们日常的工作生活中&#xff0c;或许大家都曾遇到过这样的问题&#xff0c;那么&#xff0c;了解它的解决方法是非常必要的。 首先&#xff0c;让我们来了解一下msvcr120.dll是什么文件。简单来说&am…

基于springboot实现桥牌计分管理系统项目【项目源码】

基于springboot实现桥牌计分管理系统演示 JAVA简介 JavaScript是一种网络脚本语言&#xff0c;广泛运用于web应用开发&#xff0c;可以用来添加网页的格式动态效果&#xff0c;该语言不用进行预编译就直接运行&#xff0c;可以直接嵌入HTML语言中&#xff0c;写成js语言&#…

单链表按位序与指定结点 删除

按位序删除(带头结点) #define NULL 0 #include<stdlib.h>typedef struct LNode {int data;struct LNode* next; }LNode, * LinkList;//按位序删除&#xff08;带头结点&#xff09; bool ListInsert(LinkList& L, int i, int& e) {if (i < 1)return false;L…

js运算,笔试踩坑知识点

文章目录 前端面试系列运算符记住口诀先计算 后 赋值赋值从右向左 和 - -计算从左向右括号里的加减优先于括号外的乘除交换俩数的值答案 前端面试系列 js运算 笔试踩坑知识点 前端js面试题 &#xff08;三&#xff09; 前端js面试题&#xff08;二&#xff09; 前端js面试题 (…

基于SpringBoot的SSMP整合案例(开启日志与分页查询条件查询功能实现)

开启事务 导入Mybatis-Plus框架后&#xff0c;我们可以使用Mybatis-Plus自带的事务&#xff0c;只需要在配置文件中配置即可 使用配置方式开启日志&#xff0c;设置日志输出方式为标准输出mybatis-plus:global-config:db-config:table-prefix: tb_id-type: autoconfiguration:…

【原创课设】java+swing+mysql选课管理系统设计与实现

摘要&#xff1a; 随着学校规模的扩大和课程设置的多样化&#xff0c;传统的手工选课管理方式已经无法满足现代教育的需求。因此&#xff0c;开发一款高效、便捷的选课管理系统变得尤为重要。该系统可以提高选课工作的效率&#xff0c;减少人为错误&#xff0c;同时也能为学生…

工业摄像机参数计算

在工业相机选型的时候有点懵&#xff0c;有一些参数都不知道咋计算的。有些概念也没有区分清楚。‘’ 靶面尺寸 CMOS 或者是 CCD 使用几分之几英寸来标注的时候&#xff0c;这个几分之几英寸计算的是什么尺寸&#xff1f; 一开始我以为这个计算的就是靶面的实际对角线的尺寸…