目录
在网络学完HTTP协议,前端学完html,css,js,后端学完Servlet开发后,做一个博客系统,巩固一下所学知识,并将所学知识运用到实际当中,以此来进一步提升对学习编程的兴趣
登陆页面
输入用户和密码,点击提交,登陆成功后跳转到博客列表页面
点击写博客,跳转到博客编辑页面
输入文章标题和文章内容,点击发布文章,发布成功后跳转到博客列表页面
在博客列表页面,点击刚发布文章的显示全文,就会显示刚才发布文章的全部内容
点击注销又跳转到博客登陆页面
这里附上静态页面设计的码云地址,可以点击查看,本篇文章只展示后端代码与前端ajax交互的部分,想要查看博客系统页面设计代码,请点击:个人博客系统的页面设计代码
创建Maven项目在pom.xml中添加项目依赖
- "1.0" encoding="UTF-8"?>
- <project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0modelVersion>
-
- <groupId>org.examplegroupId>
- <artifactId>my-blogartifactId>
- <version>1.0-SNAPSHOTversion>
-
- <properties>
- <maven.compiler.source>8maven.compiler.source>
- <maven.compiler.target>8maven.compiler.target>
- properties>
-
- <dependencies>
- <dependency>
- <groupId>javax.servletgroupId>
- <artifactId>javax.servlet-apiartifactId>
- <version>3.1.0version>
- dependency>
- <dependency>
- <groupId>mysqlgroupId>
- <artifactId>mysql-connector-javaartifactId>
- <version>5.1.49version>
- dependency>
- <dependency>
- <groupId>com.fasterxml.jackson.coregroupId>
- <artifactId>jackson-databindartifactId>
- <version>2.12.3version>
- dependency>
-
- <dependency>
- <groupId>junitgroupId>
- <artifactId>junitartifactId>
- <version>4.13.1version>
- dependency>
- dependencies>
- <build>
- <finalName>my-blogfinalName>
- build>
- project>
创建与开发相关的包
引入前端资源
将前端资源都放在main/webapp目录下,前端资源从上面码云地址中获取,web.xml存放在main/webapp/WEB-INF目录下
创建表的时候可以插入一些数据便于后续的测试
- drop database if exists blog;
- create database blog character set utf8mb4;
- use blog;
- create table user(
- id int primary key auto_increment,
- username varchar(20) not null unique,
- password varchar(20) not null,
- nickname varchar(10) not null
- );
- insert into user values(null,'abc','123','糯米');
-
- create table article(
- id int primary key auto_increment,
- title varchar(50) not null,
- `date` date,
- content mediumtext,
- user_id int,
- foreign key (user_id) references user(id)
- );
- insert into article values(null,'文章1','2022-9-9','今天要好好学习',1);
- insert into article values(null,'文章2','2022-9-17','今天要玩游戏',1);
1. 创建数据库工具类DBUtil,提供获取数据库连接和统一释放资源
- import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
-
- import javax.sql.DataSource;
- import java.sql.Connection;
- import java.sql.ResultSet;
- import java.sql.SQLException;
- import java.sql.Statement;
-
- //数据库工具类,提供获取数据库连接,释放资源统一代码
- public class DBUtil {
-
- //一个程序,连接一个数据库,只需要一个连接池,其中保存了多个数据库连接对象
- private static MysqlDataSource ds; //静态变量,类加载时执行初始化,只执行一次
- //获取连接池,内部使用,不开放
- private static DataSource getDataSource(){
- if(ds == null){
- ds = new MysqlDataSource();
- ds.setURL("jdbc:mysql://127.0.0.1:3306/blog");
- ds.setUser("root");
- ds.setPassword("xiaobai520..@@@");
- ds.setUseSSL(false); //不安全连接,不设置会有警告
- ds.setCharacterEncoding("UTF-8");
- }
- return ds;
- }
-
- //获取数据库连接对象,开放给外部的jdbc代码使用
- public static Connection getConnection(){
- try {
- return getDataSource().getConnection();
- } catch (SQLException e) {
- throw new RuntimeException("获取数据库连接报错",e);
- }
- }
-
- //释放资源,查询操作需要释放三个资源
- public static void close(Connection c, Statement s, ResultSet rs){
- try {
- if(rs != null) rs.close();
- if(s != null) s.close();
- if(c != null) c.close();
- } catch (SQLException e) {
- throw new RuntimeException("释放数据库资源出错",e);
- }
- }
-
- //更新操作释放两个资源
- public static void close(Connection c,Statement s){
- close(c,s,null);
- }
- }
2. 创建Web工具类WebUtil,提供一个类专门检查用户是否登陆,还提供序列化与反序列化类,用来将java对象与json字符串相互转化
- import com.fasterxml.jackson.core.JsonProcessingException;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import org.example.model.User;
-
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpSession;
- import java.io.IOException;
- import java.io.InputStream;
-
- public class WebUtil {
- public static User checkLogin(HttpServletRequest req){
- User user = null;
- HttpSession session = req.getSession(false);
- user = (User) session.getAttribute("user");
- return user;
- }
-
- //使用单例
- private static ObjectMapper mapper = new ObjectMapper();
-
- //反序列化:json字符串转换Java对象
- //使用泛型,传一个什么类型,就返回该类型的对象
- //泛型方法:方法限定符 <类型型参列表> 返回值类型 方法名
- public static
T read(InputStream is,Class clazz) { - try {
- return mapper.readValue(is,clazz);
- } catch (IOException e) {
- throw new RuntimeException("json反序列化出错",e);
- }
- }
-
- //序列化:将java对象转化为json字符串
- public static String write(Object o){
- try {
- return mapper.writeValueAsString(o);
- } catch (JsonProcessingException e) {
- throw new RuntimeException("json序列化出错",e);
- }
- }
- }
每张用户表对应有一个实体类,所以创建User和Article类,创建类的时候提供Getter和Setter方法并且重写toString方法
用户类User
- public class User {
- private Integer id;
- private String username;
- private String password;
- private String nickname;
-
- @Override
- public String toString() {
- return "User{" +
- "id=" + id +
- ", username='" + username + '\'' +
- ", password='" + password + '\'' +
- ", nickname='" + nickname + '\'' +
- '}';
- }
-
- public Integer getId() {
- return id;
- }
-
- public void setId(Integer id) {
- this.id = id;
- }
-
- public String getUsername() {
- return username;
- }
-
- public void setUsername(String username) {
- this.username = username;
- }
-
- public String getPassword() {
- return password;
- }
-
- public void setPassword(String password) {
- this.password = password;
- }
-
- public String getNickname() {
- return nickname;
- }
-
- public void setNickname(String nickname) {
- this.nickname = nickname;
- }
- }
文章类Article
- import java.util.Date;
-
- public class Article {
- private Integer id;
- private String title;
- private Date date; //时间
- private String content;
- private Integer userId;
- private String dateString;//日期字符串
-
- @Override
- public String toString() {
- return "Article{" +
- "id=" + id +
- ", title='" + title + '\'' +
- ", date=" + date +
- ", content='" + content + '\'' +
- ", userId=" + userId +
- ", dateString='" + dateString + '\'' +
- '}';
- }
-
- public Integer getId() {
- return id;
- }
-
- public void setId(Integer id) {
- this.id = id;
- }
-
- public String getTitle() {
- return title;
- }
-
- public void setTitle(String title) {
- this.title = title;
- }
-
- public Date getDate() {
- return date;
- }
-
- public void setDate(Date date) {
- this.date = date;
- }
-
- public String getContent() {
- return content;
- }
-
- public void setContent(String content) {
- this.content = content;
- }
-
- public Integer getUserId() {
- return userId;
- }
-
- public void setUserId(Integer userId) {
- this.userId = userId;
- }
-
- public String getDateString() {
- return dateString;
- }
-
- public void setDateString(String dateString) {
- this.dateString = dateString;
- }
- }
Java对象JsonResult类
后端返回给前端的响应时返回的是json字符串,所以需要一个JsonResult类,该类的字段保存操作是否成功和要返回给前端的数据,后端在返回给前端json字符串的时候只需要将该类序列化为json字符串返回给前端,该类也得提供Getter与Setter方法并且重写toString方法
- public class JsonResult {
- private boolean ok;//标识执行一个操作是否成功
- private Object data;//操作成功,且是一个查询操作,需要返回一些数据给前端
-
- @Override
- public String toString() {
- return "JsonResult{" +
- "ok=" + ok +
- ", data=" + data +
- '}';
- }
-
- public boolean isOk() {
- return ok;
- }
-
- public void setOk(boolean ok) {
- this.ok = ok;
- }
-
- public Object getData() {
- return data;
- }
-
- public void setData(Object data) {
- this.data = data;
- }
- }
前后端业务逻辑的实现顺序:
说明:后面实现的顺序是先前端再后端,但是观看参考时,建议按照上面的逻辑顺序,因为实际开发时就是按照这个顺序来开发的
所有的请求都是使用ajax发送http请求,所以将封装的ajax函数写在一个js文件中,后续发送请求时只需将封装的ajax函数引入即可
封装的ajax函数
- //封装ajax函数,args为一个js对象
- //args对象属性如下:
- //method:请求方法,url:请求资源路径,contenType:请求正文格式
- //body:请求正文,callback:回调函数,客户端接收到响应数据后调用
- function ajax(args) {
- let xhr = new XMLHttpRequest();
- //设置回调函数
- xhr.onreadystatechange = function () {
- //4:客户端接收到服务端响应
- if (xhr.readyState == 4) {
- //回调函数可能会使用响应的内容,作为传入参数
- args.callback(xhr.status, xhr.responseText);
- }
- }
- xhr.open(args.method, args.url);
- //如果args中contentType有内容,就设置Content-Type请求头
- if (args.contentType) {//js中if可以判断是否有值
- xhr.setRequestHeader("Content-Type", args.contentType);
- }
- //如果args中body有内容,设置body请求正文
- if (args.body) {
- xhr.send(args.body);
- } else {
- xhr.send();
- }
- }
前端设计
- <script src="js/util.js">script>
- <script>
- let submit = document.querySelector("#submit");
- //绑定提交按钮点击事件
- submit.onclick = function(){
- let username = document.querySelector("#username").value;
- let password = document.querySelector("#password").value;
- //发送ajax请求,需要设置method,url,contentType,body
- ajax({
- method: "post",
- url: "login",
- contentType: "application/json",
- body: JSON.stringify({
- //冒号前是前后端约定的键,冒号后是变量值
- username: username,
- password: password
- }),
- callback: function(status,responseText){
- if(status == 200){
- let json = JSON.parse(responseText);
- if(json.ok){
- alert("登陆成功");
- window.location.href = "blog_list.html";
- }else {
- alert("账号或密码错误");
- }
- }else {
- alert("响应状态码:"+status+"/nbody:"+responseText);
- }
- }
- });
- }
- script>
后端Servlet设计
- @WebServlet("/login")//登陆
- public class LoginServlet extends HttpServlet {
- //登陆功能,json提交{username:abc,password:123}
- @Override
- protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
- //解析请求:通过输入流获取请求数据
- req.setCharacterEncoding("utf-8"); //设置请求对象的编码格式
- InputStream is = req.getInputStream();
- //将输入流中的json字符串转化为java对象
- //使用ObjectMapper将java对象和json字符串相互转换,Servlet都要用,封装到WebUtil中
- User get = WebUtil.read(is,User.class);
- //在数据库校验账号密码:通过账号密码在数据库查用户,若能查到则账号密码正确
- User user = UserDao.isLogin(get.getUsername(),get.getPassword());
- //不管登陆是否成功,返回的http响应正文(body)都是json字符串
- //需要设计一个类,这个类的成员变量属性,用于前端ajax解析响应
- //先创建一个响应正文需要的Java对象,然后在转换为json字符串,再设置到响应正文
- JsonResult json = new JsonResult();
- if(user != null){
- //登陆成功,设置session
- HttpSession session = req.getSession(true);
- session.setAttribute("user",user);
- //设置json对象中,操作是否成功为true
- json.setOk(true);
-
- }else {
- //登陆失败,设置操作是否成功字段为false
- json.setOk(false);
- }
- //设置响应正文的格式
- resp.setContentType("application/json; charset=utf-8");
- resp.getWriter().write(WebUtil.write(json));
- }
- }
前端设计
客户端展示页面时,就需要展示文章列表数据,加载完页面就发生ajax请求,来获取文章列表内容,待收到返回的响应执行回调时,解析响应的文章列表数据
- <script src="js/util.js">script>
- <script>
- //客户端展示页面时,就需要展示文章列表数据
- //加载完页面,就发送ajax请求获取文章列表的数据
- //返回响应执行回调时,解析响应的文章列表数据
- ajax({
- method: "get",
- url: "blog_list",
- callback: function(status,responseText){
- if(status == 200){
- let json = JSON.parse(responseText);
- if(json.ok){
- let data = json.data;
- let nickname = data.nickname;
- let h3 = document.querySelector(".card>h3");
- h3.innerHTML = nickname;
- let articles = data.articles;
- let div = document.querySelector(".container-right");
- //str不赋值就是undefined,再去拼接字符串就会出错
- let str = "";
- for(let a of articles){
- //``里面可以包括单引号和双引号
- str += ``;
- str += ``;
- str += a.title;
- str += ``;
- str += ``;
- str += a.dateString;
- str += ``;
- str += ``;
- str += `
`
; - str += a.content;
- str += ``;
- str += ``;
- str += ``;
- }
- div.innerHTML = str;
- let num = data.count;
- let count = document.querySelector("#count");
- count.innerHTML = num;
- }else {
- alert("ok==false");
- }
- }else {
- alert("响应状态码:"+status+"/nbody:"+responseText)
- }
- }
- });
- script>
- html>
后端Servlet设计
- @WebServlet("/blog_list")//博客列表
- public class BlogListServlet extends HttpServlet {
- @Override
- protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
- //未登录,访问跳转到登陆页面
- User user = WebUtil.checkLogin(req);
- if(user == null){
- resp.sendRedirect("login.html");
- return; //未登录,直接跳转,不会执行后边逻辑
- }
- //通过登陆用户id查找所有文章
- List
articles = ArticleDao.selectById(user.getId()); - //通过登陆用户id查找文章数目
- int count = ArticleDao.getCount(user.getId());
-
- //先构造响应正文需要的java对象,再转化为json字符串,再设置到响应正文
- JsonResult json = new JsonResult();
- json.setOk(true);
- //前端需要的数据有nickname,articles,可以用map保存,然后设置到json.data中
- Map
data = new HashMap<>(); - data.put("nickname",user.getNickname());
- data.put("articles",articles);
- data.put("count",count);
- json.setData(data);
-
- resp.setContentType("application/json; charset=utf-8");
- resp.getWriter().write(WebUtil.write(json));
- }
- }
前端设计
博客详情是从博客列表的显示全文按钮跳转过来的,所以跳转的连接携带id,id标识文章id,表示显示的是哪篇文章的全部内容
- <script src="js/util.js">script>
- <script>
- //window.location.search获取的是queryString?后的部分,blog_content?id=1
- //也就是获取的是?id=1
- let id = window.location.search.substring(4);
- //页面一加载就需要展示博客详情页面,所以就发送ajax请求来获取内容
- ajax({
- method: "get",
- url: "blog_content?id="+id,
- callback: function(status,responseText){
- if(status == 200){
- let json = JSON.parse(responseText);
- if(json.ok){
- let data = json.data;
- let nickname = data.nickname;
- let h3 = document.querySelector(".card>h3");
- h3.innerHTML = nickname;
- let num = data.count;
- let count = document.querySelector("#count");
- count.innerHTML = num;
- let article = data.article;
- let div = document.querySelector(".detail");
- let str = "";
- str += ``;
- str += article.title;
- str += ``;
- str += ``;
- str += article.dateString;
- str += ``;
- str += ``;
- //str += article.content;
- str += ``;
- div.innerHTML = str;
- //不能直接展示markdown源码,数据库保存的是markdown源码
- editormd.markdownToHTML("article-content",{markdown: article.content});
- }else {
- alert("ok == false");
- }
- }else {
- alert("响应状态码:"+status+"/nbody:"+responseText);
- }
- }
- });
- script>
后端Servlet设计
- @WebServlet("/blog_content")//博客详情
- public class BlogContentServlet extends HttpServlet {
- @Override
- protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
- //未登录,访问跳转到登陆页面
- User user = WebUtil.checkLogin(req);
- if(user == null){
- resp.sendRedirect("login.html");
- return; //未登录,直接跳转,不会执行后边逻辑
- }
-
- //路径为:blog_content?id=文章id
- //解析请求
- String sid = req.getParameter("id"); //获取到文章id
- //通过文章id查询整篇文章
- Article a = ArticleDao.queryById(Integer.parseInt(sid));
- int count = ArticleDao.getCount(user.getId());
-
- JsonResult json = new JsonResult();
- json.setOk(true);
- Map
data = new HashMap<>(); - data.put("nickname",user.getNickname());
- data.put("article",a);
- data.put("count",count);
- json.setData(data);
- resp.setContentType("application/json; charset=utf-8");
- resp.getWriter().write(WebUtil.write(json));
- }
- }
前端设计
- <script src="js/util.js">script>
- <script src="js/jquery.min.js">script>
- <script src="editor.md/lib/marked.min.js">script>
- <script src="editor.md/lib/prettify.min.js">script>
- <script src="editor.md/editormd.min.js">script>
- <script>
- $(function(){
- var editor = editormd("edit-content",{
- width: "100%",
- height: "calc(100% - 50px)",
- markdown: "# 在这里写下第一篇博客",
- path: "editor.md/lib/",
- saveHTMLToTextarea: true
- });
- })
- //发布文章点击事件
- function addContent(){
- let title = document.querySelector("#title").value;
- let content = document.querySelector("#content").value;
- ajax({
- method: "post",
- url: "blog_add",
- contentType: "application/json",
- body: JSON.stringify({
- title: title,
- content: content
- }),
- callback: function(status,responseText){
- if(status == 200){
- let json = JSON.parse(responseText);
- if(json.ok){
- alert("发布文章成功");
- window.location.href = "blog_list.html";
- }else {
- alert("ok == false");
- }
- }else {
- alert("响应状态码:"+status+"/nbody:"+responseText);
- }
- }
- });
- }
- script>
后端Servlet设计
- @WebServlet("/blog_add")//添加文章
- public class BlogAddServlet extends HttpServlet {
- @Override
- protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
- User user = WebUtil.checkLogin(req);
- if(user == null){
- //未登录不允许访问
- resp.sendRedirect("login.html");
- return;
- }
- //解析请求
- req.setCharacterEncoding("utf-8");
- InputStream is = req.getInputStream();
- Article beInsert = WebUtil.read(is,Article.class);
- //数据库插入一条数据,相当于插入一个对象
- beInsert.setUserId(user.getId());
- beInsert.setDate(new java.util.Date());
- int n = ArticleDao.insertOne(beInsert);
- JsonResult json = new JsonResult();
- json.setOk(true);
- resp.setContentType("application/json; charset=utf-8");
- resp.getWriter().write(WebUtil.write(json));
- }
- }
- @WebServlet("/logout")
- public class LogoutServlet extends HttpServlet {
- @Override
- protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
- //注销就是通过session对象删除保存的用户信息
- HttpSession session = req.getSession(false);
- if(session != null){
- session.removeAttribute("user");
- }
- resp.sendRedirect("login.html");
- }
- }
前端后端做业务处理的数据库逻辑操作如下:
通过登陆输入的用户名和密码查询到user并将user返回,以此来做用户密码校验功能
- public class UserDao {
- public static User isLogin(String username, String password){
- Connection c = null;
- PreparedStatement ps = null;
- ResultSet rs = null;
- try {
- c = DBUtil.getConnection();
- String sql = "select * from user where username=? and password=?";
- ps = c.prepareStatement(sql);
- ps.setString(1,username);
- ps.setString(2,password);
- rs = ps.executeQuery();
- User user = null;
- while(rs.next()){
- user = new User();
- int id = rs.getInt("id");
- String nickname = rs.getString("nickname");
- user.setId(id);
- user.setNickname(nickname);
- user.setUsername(username);
- user.setPassword(password);
- }
- return user;
- } catch (SQLException e) {
- throw new RuntimeException("校验账号密码出错",e);
- } finally {
- DBUtil.close(c,ps,rs);
- }
- }
-
- @Test
- public void testLogin(){
- System.out.println(isLogin("abc","123"));
- }
- }
通过用户id查询所有文章
- //通过用户id查询所有文章
- public static List
selectById(Integer id){ - List
articles = new ArrayList<>(); - Connection c = null;
- PreparedStatement ps = null;
- ResultSet rs = null;
- try{
- c = DBUtil.getConnection();
- String sql = "select * from article where user_id=?";
- ps = c.prepareStatement(sql);
- ps.setInt(1,id);
- rs = ps.executeQuery();
- while(rs.next()){
- Article a = new Article();
- a.setId(rs.getInt("id"));
- a.setTitle(rs.getString("title"));
- java.sql.Date date = rs.getDate("date");
- long time = date.getTime();
- a.setDate(new java.util.Date(time));
- DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
- String dateString = df.format(a.getDate());
- a.setDateString(dateString);
- String content = rs.getString("content");
- a.setContent(content.length()>50 ? content.substring(0,50) : content);
- a.setUserId(id);
- articles.add(a);
- }
- return articles;
- } catch (SQLException e) {
- throw new RuntimeException("查询文章出错",e);
- } finally {
- DBUtil.close(c,ps,rs);
- }
- }
根据文章id查询整篇文章
- //根据文章id查文章
- public static Article queryById(int id) {
- Article a = null;
- Connection c = null;
- PreparedStatement ps = null;
- ResultSet rs = null;
- try{
- c = DBUtil.getConnection();
- String sql = "select * from article where id=?";
- ps = c.prepareStatement(sql);
- ps.setInt(1,id);
- rs = ps.executeQuery();
- while(rs.next()){
- a = new Article();
- a.setId(id);
- a.setTitle(rs.getString("title"));
- java.sql.Date date = rs.getDate("date");
- DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
- String dateString = df.format(new java.util.Date(date.getTime()));
- a.setDateString(dateString);
- a.setContent(rs.getString("content"));
- a.setUserId(rs.getInt("user_id"));
- }
- return a;
- } catch (SQLException throwables) {
- throw new RuntimeException("查询文章详情jdbc出错",throwables);
- } finally{
- DBUtil.close(c,ps,rs);
- }
- }
插入一篇文章
- //插入文章
- public static int insertOne(Article a) {
- Connection c = null;
- PreparedStatement ps = null;
- try{
- c = DBUtil.getConnection();
- String sql = "insert into article(title,`date`,content,user_id) values(?,?,?,?)";
- ps = c.prepareStatement(sql);
- ps.setString(1,a.getTitle());
- //ps.setDate(2,new java.sql.Date(a.getDate().getTime()))
- ps.setDate(2,new java.sql.Date(System.currentTimeMillis()));
- ps.setString(3,a.getContent());
- ps.setInt(4,a.getUserId());
- return ps.executeUpdate();
- } catch (SQLException throwables) {
- throw new RuntimeException("发布文章jdbc出错",throwables);
- } finally {
- DBUtil.close(c,ps);
- }
- }
获取文章数目
- public static int getCount(Integer id) {
- Connection c = null;
- PreparedStatement ps = null;
- ResultSet rs = null;
- try{
- c = DBUtil.getConnection();
- String sql = "select 0 from article where user_id=?";
- ps = c.prepareStatement(sql);
- ps.setInt(1,id);
- rs = ps.executeQuery();
- int count = 0;
- while(rs.next()){
- count++;
- }
- return count;
- } catch (SQLException throwables) {
- throw new RuntimeException("查询文章数目出错",throwables);
- } finally{
- DBUtil.close(c,ps,rs);
- }
- }
在做前后端逻辑处理的时候,前端代码有些稍微的改动,本文没有提及到,请点击查看源码,查看改动的细节以及所有后端的设计实现:个人博客系统设计源码