本篇会加入个人的所谓鱼式疯言
❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言
而是理解过并总结出来通俗易懂的大白话,
小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的.
🤭🤭🤭可能说的不是那么严谨.但小编初心是能让更多人能接受我们这个概念 !!!
博客系统线上项目,请点击
引言
基于SpringBoot快速构建三层架构体系,Controller层作为用户请求的交通枢纽,Service层专注业务逻辑精炼,Mapper层架起数据存储桥梁。JWT令牌实现无状态安全认证,MySQL关系型数据库确保数据持久化。通过分层解耦和模块化设计,使系统具备弹性扩展能力,为个人博客打造坚实可靠的技术基座。
目录
-
项目展示与需求分析
-
项目分析
-
环境配置
-
前端代码模块
-
公共模块
-
注册登录模块
-
验证码模块
-
用户模块
-
博客模块
-
源码展示
一. 项目展示与需求分析
1. 项目展示
在这个大部分人都是分享欲满满的时代,编写一篇属于自己的思想或美好,并且分享给那些需要分享的人无疑就是一件幸福的事情~
在当下,编写内容,发布文章的方式有很多,但是这次小编带来了能够 随时发布分享,更新,删除的项目《博客系统》, 不仅能够满足当下众人的分享欲,也为吸引流量提供了更多的商机~
先看看咱们的项目长啥样呗~
登录
注册
验证码
博客列表
博客详情
博客更新
博客删除
博客编辑
2. 需求分析
- 首先可以点击登录页面的下方链接,用户先注册一条账号
(注意这里注册的账号必须是未注册的账号)
~
- 注册成功之后,跳转回登录页面,用户输入刚注册成功的用户名和密码。
- 接着输入正常的用户名密码之后,跳转到验证码页面,用户根据验证码的提示输入正确的验证码。
- 然后验证码输入成功之后,跳转到 博客列表页面。用户在博客列表页中
(可以翻页查询)
,可以选择:
菜单模块
: 用户可以选择主页功能(博客列表页)
,写博客(博客编辑页面)
,退出登录(登录页面)
博客模块
:用户可以点击某篇模块进入 博客详情页,如果是当前登录的用户博客,可进行删除或更新博客
用户信息模块
:如果是在文章列表, 用户信息展示的是当前登录的用户信息; 如果是文章详情,用户信息展示的当前文章作者信息。
- 用户点击
菜单模块
,可以选择写文章,编辑好内容可以点击发布文章,就会 跳转到列表页面,展示最新发布的文章。
- 用户点击列表页的某篇文章,查看作者信息,如果作者信息为当前登录用户信息, 可以选择
更新文章
或者删除文章
~ 如果不是只能欣赏文章~
二. 项目分析
1. 架构分析
前端技术:HTML
、CSS
、JS
、JQuery
、AJAX
等…
后端技术:SpringFrame
、SpringBoot
、SpringMVC
、MySQL
、Mybatis-Plus
、JWT
等…
- 业务代码上进行采用
Spring MVC
思想,采用分层架构,分为Controller
、Service
、Mapper
三层, 在 设计上达到高内聚低耦合的优点。
- 分别使用
request
、DO
、Response
对象管理,减少代码之间的耦合度。
- 采用
Spring AOP
的思想,实现统一结果或异常返回 ,便于前后端的交互的 类型统一和代码分离。
- 前端页面使用
HTML与CSS
搭建页面,使用JS
脚本建立 前后端交互,并且前端使用AJAX
发送请求,
MySQL
对数据进行 持久化存储,使用Mybatis-Plus
框架连接数据库与业务代码的交互与操作, 从而有效提高开发效率。
- 对
登录模块
使用JWT
生成Token
作为 登录凭证, 配合拦截器实现看门狗模式 保证 系统的安全性 。
- 对密码这种敏感数据,采用
MD5算法 + 盐值
的方式 对明文进行加密 进行密文 + 盐值的方式持久化存储。 防止数据库的密码明文存储,发生泄露, 保证整体系统的安全性!
2. 模块设计
对应新用户,必然从注册页面走起:
- 注册页面
- 用户名:输入没注册过的用户名
- 密码:输入注册密码长度不得超过18位
- 重复密码:输入重复密码必须保证与密码一致。
- 登录页面
-
上方栏右上角:展示菜单模块:
- 点击“主页”, 跳转到博客列表页面(未登录重新跳转回登录页面)
- 点击“写博客”, 跳转到博客编辑页面(发布时跳转回登录页面)
- 点击“退出登录”, 跳转到登录页面
-
用户名:输入必须是已注册过的用户名
-
密码:输入密码必须是对应用户名下的正确的密码才能登录成功!
- 博客列表
-
上方栏右上角:展示菜单模块:
- 点击“主页”, 跳转到博客列表页面
- 点击“写博客”, 跳转到博客编辑页面
- 点击“退出登录”, 跳转到登录页面
-
主栏左边:展示当前登录的用户名和头像, 还有 github 地址, 点击后跳转到码云页面, 并且还展示当前的总篇数和总人数。
-
主栏右边:采用分页展示,展示当前页数的所有博客,每一条博客包括:
- 博客标题
- 博客时间
- 博客正文(只展示一部分内容)
- 查看全文选项
点击查看全文选项,进入博客详情页面:
- 博客详情
-
上方栏右上角:展示菜单模块:
- 点击“主页”, 跳转到博客列表页面
- 点击“写博客”, 跳转到博客编辑页面
- 点击“退出登录”, 跳转到登录页面
-
主栏左边:展示当前文章作者的信息, 还有github地址, 点击后跳转到码云页面, 并且还展示当前的总篇数和总人数。
-
主栏右边:只展示当前文章正文的所有内容~
-
如果作者用户名与登录用户名一致,提供文章更新功能与删除功能:
- 点击更新功能,进入编辑页更新
- 点击删除功能,可选择确定或取消
- 博客编辑
分两情况
-
更新文章:
- 提供原有文章标题与正文, 可进行修改,
- 点击更新文章,跳转回列表页面查看。
-
新文章发布:
- 标题为空(必须填写)
- 正文有初始化, 可以删除后重新编辑
- 点击发布文章, 即可发布成功一篇文章,回到列表页查看。
3. 数据库设计
如上图:
用户表
user_info :
- 主键:
id
- 用户名:
user_name
- 密码:
password
- 安全密码:
safe_password
- github地址:
github_url
- 图片地址:
picture
- 是否删除:
delete_flag
(0为未删除,1为已删除) - 创建时间:
create_time
- 更新时间:
update_time
博客表
blog_info:
- 主键:
id
- 标题:
title
- 正文:
content
- 用户id:
user_id
- 是否删除:
delete_flag
(0为未删除,1为已删除) - 创建时间:
create_time
- 更新时间:
update_time
三. 环境配置
1. 项目创建与依赖配置
创建项目
配置基础依赖
加入剩下依赖
2. 添加配置到配置文件
在本项目中配置文件总共有三个
分为:核心配置文件,开发环境配置文件,线上环境配置文件。
为啥需要三种配置文件,主要是因为:
核心配置文件:其
控制管理
的作用,用来 指定在什么环境下需要用哪个配置文件
核心配置文件URL
开发环境配置文件:指的是在
开发环境
下需要 配置的各种实际参数等…
线上配置文件:指的是在
线上环境
下需要 配置的各种实际参数 等…
关于配置文件这些,小伙伴只要了解即可,不需要重点掌握哦~
鱼式疯言
可能小伙伴们有所不知,在整个软件开发的周期中,存在着多个环境:
如图:
一般在企业中:
开发人员在
开发环境
下编写代码,然后进行自测,发现没有BUG就可以进入测试环境(开发人员自己接触软件)
接下来测试人员在
测试环境
下测试开发人员开发的代码,如果没有问题就可以进入下一步。(一小部分使用软件)
最终可以在
线上/生产环境
来上线咱们的项目,到了线上环境可就由所有的用户使用软件。一般由我们的开发人员或运维人员来维护咱们的软件~
所以,从上面可以看出 开发环境,测试环境,线上环境 是很有很大差别的~
并且 线上环境是最终的环境,并且是
最重要的环境
,一定要保证在线上环境下的所有配置是没有问题的,所以需要单独写一个配置文件而不能和开发环境冲突。
3. 数据库创建表
在上面这段 sql
中主要操作:
- 建立
java_blog_spring
数据库并使用
- 删除
user_info
表并重新创建user_info
表结构
- 删除
blog_info
表并重新创建blog_info
表结构
- 最终分别往
user_info
和blog_info
表插入数据
四. 前端代码模块
咱们作为后端开发人员,对于前端代码这里不做掌握哦~
感兴趣的小伙伴可以去下面这个网站学习哦~
五. 共同模块
1. 统一结果返回与统一异常处理
统一结果处理
- 首先定义一个
ResponseAdvice
类,并且实现ResponseBodyAdvice
接口
- 其次在 类 的上面使用
@ControllerAdvice
告诉Spring, 从控制层返回出来的结果需要经过这个方法。- 然后先实现
supports
, 并且返回true
, 表示需要 统一结果返回。
- 最后实现
beforeBodyWrite
, 获取正文body
数据,判断类型,统一类型进行返回~
统一异常处理
- 首先定义一个
ExceptionAdvice
类,并且在该类的上方加上@ControllerAdvice
以及@ResponseBody
,@ResponseBody
是告诉Spring
需要以JSON
的格式来效应给前端。
- 对每一个方法加上
@ExceptionHandler(不同异常的类对象)
,标记需要捕获哪个异常或子类异常
- 对捕获的异常进行统一的异常信息的处理和返回。
鱼式疯言
可能有小伙伴们还不是很理解Spring AOP 的思想, 下面小编就说点大白话给小伙伴们补充点干货~
Spring AOP 思想:简单来说就四个字: 代理模式
什么话说回来了,什么又是代理模式?
想必小伙伴们都租过房子吧~
是不是会经常遇到那种中介给你推房子, 而代理模式就好似中介给你推房子。
本来呢? 你是直接找房子的主人也就是房东去租房子的, 这相当于属于你的业务逻辑吧~
但是你初来乍到, 对找房子这件事情又没有什么经验, 所有就得找一个靠谱的, 所以这时就找了一个中介, 让中介帮你处理这些事情, 也就是在你原有的业务逻辑的前提上加上了中介帮你代理这些事情。 所以我们把这些行为就称之为 代理模式
。
在
Spring AOP
中的代理模式也是如此,比如上述的过程中, 我们在不影响整体的业务逻辑下, 在处理业务逻辑前或后, 就创建一个代理对象, 当某些特定的事务或者情况出现时(称之为PointCut
), 就可以执行某些特定的行为。(称之为Advice
) 而把这些出现的PointCut + Advice
的整个过程称之为切面
, 所以Spring AOP
又称之为 面向切面编程。
如上图,在本项目中,
在业务逻辑执行前,我们实现
interceptor
, 当拦截某个指定的接口(可以认为是PointCut
), 我们就进行身份校验Token
(可以认为是Advice
)。
在业务逻辑执行过程中, 一旦发生了异常, 借助
@ExceptionHandler
捕获异常(可以认为是PointCut
), 我们就 统一异常返回 (可以认为是Advice
)
在业务逻辑执行执行结束后, 当返回结果数据(可以认为是
PointCut
), 我们就 对结果数据 统一结果返回 (可以认为是Advice
)。
所以可以看出, 如果
PointCut
和Advice
相互配合使用,形成切面, 不仅能达到全局异常的统一, 更能达到API 的一致性和代码的简化。
2. 拦截器的定义与配置
在
Spring
中,使用拦截器需要先初始化拦截器,然后注册进Web配置
中
<1>.实现拦截器的行为
- 首先创建一个
LoginInterceptor
并且实现HandlerInterceptor
接口,再其类上方加上@Component
- 其次实现
preHandle
这个方法,从request
这个参数中获取请求头数据
- 然后从请求头中获取
token
,并且通过 实现的JWT
单元获取token
的载荷部分。
- 最后如果 载荷不为空 就说明
Token
凭证有效,身份认证成功,返回true
。 否则登录失效, 返回false
和 状态码401
,
<2>. 将拦截器注册进Web配置
- 实现创建一个
WebConfig
类并且实现WebMvcConfigurer
,并且带上@Configuration
- 其次实现一个
addInterceptors
这个方法, 并且在registry
这个参数使用addInterceptor
方法通过Spring
管理的 拦截器注册进入。
- 最后添加需要 拦截的路径, 以及 不需要拦截的路径(使用List来组织)。
3. 枚举与类型转化
<1> 结果状态枚举
- 创建一个枚举类
ResultStatusEnums
带上@AllArgsConstructor
@NoArgsConstructor
表名需要创建 全参数的构造器 和 无参数的构造器。
- 定义一个
code
常量属性。并且创建两个枚举常量:SUCCESS
和FAIL
- 最后给
code
属性带上@Getter
和@Setter
, 表示需要得到这个code
或设置这个code
。
<2>. Request类型, DO类型,Response类型,以及类型转化
- 对于
Request
类型,一般作用于Controller
层, 作用:作为接收前端传入的参数的对象,不仅可以达到数据类型的统一, 而且整合到同一对象更好的统一管理和操作。
- 对于
DO
类型,一般作用在Mapper
层,作用:用于数据库字段和Java
属性一一映射, 便于在业务逻辑上更好的操作数据库的数据,== 一次性的拿到数据库的所有字段数据,就不需要多次读数据库==。
- 对于
Response
类型,一般作用在Service
层,作用:Service层执行完业务逻辑之后,将需要的数据都整合到Response
类型上, 能够更好控制需要的数据返回, 不需要的数据能够起到保护的作用, 同时也不会暴露数据库的库表结构~
从上面的讲解中, 小伙伴们应该清楚,对于
DO
而言,拿到的数据库表所有的信息, 而对于Response
而言只是拿到数据库表中的部分信息。
而且两个又是不同的类型, 所有这些就需要自定义实现一些转化方法,将
DO
类转化为Response
类型,方便Service
层的数据返回。
4. JWT
在本项目中,JWT
生成的Token 主要是作为 登录凭证。方便后期校验用户的身份信息。
JWT 要使用,首先要生成一个 JWT
的 Token令牌
, 并且还要有 校验令牌 的行为。
<1>. 生成Token
- 首先创建
JwtUtil
这个类,创建genToken
这个方法,设置一个Map参数
- 其次使用
Jwts
这个类的静态方法创建令牌
- 然后设置参数, 将传入的
Map
信息作为载荷, 设置签名时间, 一个固定的超时时间,从设置开发计算,一旦超过这个超时时间,Token
就会失效。
- 最后对
Token
设置密钥, 注意这里的密钥BASE64
先对原有明文密钥进行加密的新密钥。
<2>. 校验Token
- 首先判断传入的
Token
是否为空, 为空就直接返回null
- 然后Jwts 根据 当前的固定密钥 生成一个校验器。
- 其次使用校验器对传入的
Token
进行校验并且获取其载荷信息。
- 最后在校验过程中,使用
try...catch
包裹,如果校验成功就返回载荷,校验异常就提示异常信息。
鱼式疯言
这里有小伙伴们就会问了?
为啥要使用JWT令牌作为登录凭证呢?
Session-Cookie
那一套会话存储校验不香嘛?
香确实香,但是香也有香的不足, 得看什么情况下去使用才叫香。
如果对于一个 单机系统(一台主机), 使用
Session
进行会话存储,减少了Jwts令牌
生成和校验的开销,当然是香喷喷的~
但是对于 多主机的分布式系统而已, 就没有那么香了, 毕竟
Session
也有他自己的不足, 一般在一台主机上生成Session
,另外一台主机是没有对应的Session
信息的, 即使客户端拿着Cookie
上正确的的SessionId才找Session
, 但是对于分布式系统而言, 如果切换到另外的一台主机上, 也就意味着另外一台主机上没有Session
, 那么就很难验证身份信息了, 就可能重新让用户再登录一次, 显然这种是不科学的。
所以为了解决这种
Session
跨主机的问题, 聪明的程序猿们,想到一种令牌的方式。
就好比要 出门做火车, 就需要拿着你的身份证就相当于你的令牌, 然后车站人员就会根据你拿的这个令牌校验你的身份信息, 可以校验你是否购买了车票等…
对于
JWT
也是如此, 拿到身份证过程就相当于生成Token
的过程, 校验身份证的过程就相当于校验Token
的过程,
这里这里小编认为, 其本质就是因为分布式系统都拥有一组相同的密钥,相同的校验方式, 所以才能多个主机都能校验产生相同的效果~
5. 盐值加密
这里采用的盐值加密
其主要逻辑还是和 Jwts
类型类似:
-
生成密文
-
校验明文
<1>. 生成密文
- 首先创建
SecurityUtil
类,并且定义genCiphertext
方法,将password
作为参数传入
- 其次使用
UUID
生成一套盐值,(盐值 + password)
进行 MD5 加密生成密文
- 最后 对加密后的密文, 加带上
(盐值 + 密文)
返回。
<2>. 校验传入的明文
- 首先生成
checkCiphertext
这个方法, 并将前端传入明文password
作为第一个参数和数据库存储的密文 , 数据库密文作为第二个参数传入。
- 其次先获取盐值, 通过 字符串截断的方式来获取密文中的盐值。
- 然后将 盐值加入到
password
, 对(盐值+password)
进行MD5
加密得到新的一组密文
- 最后将
(盐值 + 密文 )
和数据库存储的密文
进行比对, 返回结果。
鱼式疯言
小编有话说~
- 加密
对于加密方式而言, 现在较普通常用的加密方式有三种:
对于加密方式而言, 也是不同情形的不同方案:
常见的加密方式有 : 对称加密
, 哈希加密
, 以及 盐值 + 哈希加密
对称加密:是一种
可逆
的加密方式, 一般使用AES加密算法可以实现, 如果是像手机号这种,既需要加密存储,保护用户的敏感数据, 同时也需要调用手机号去发送短信,邮件等服务就可以使用对称加密。安全程度:低
哈希加密:是一种
不可逆
的加密方式, 一般使用 MD5 或 SHA256 加密算法实现。 想密码这种,只需要加密存储, 但不需要反复调用的话,就可以直接不可逆的加密。 安全程度:高
盐值+哈希加密: 本质和哈希加密差不多, 但是 ==由于引入了盐值这个随机字符串, 所以就会使用密文复杂程度提高一个
level
== , 安全程度:最高
所以如果是一些特别重要的机密数据, 最好是使用 盐值 + 哈希
加密的方式存储,千万不要用 对称加密
, 因为一旦有别有用心之人获取到你对称加密的密钥, 就会完全掌握你的所有信息, 做出违法犯罪的事情。
- 盐值
一般来讲就是一堆随机的字符串, 那么我们就完全可以用
UUID
来生成。
可能小伙伴有所不知,
UUID
生成 每次随机生成的字符串是全球的唯一性。也就是说全球都找不出第二个相同的UUID
,随机性是很高的。
所以使用
UUID
作为盐值,可以 有效的增加密码的破解难度, 增加系统的安全性。
六. 注册登录模块
1. 注册模块
<1>. 约定前后端交互接口
<2>. 梳理实现逻辑
如上方时序图:
- 首先用户发送一个注册请求,在通过
Controller
层传入Service
层进行校验
- 校验成功之后, 将 密文通过加密单元加密成密文
- 将 用户名和密文数据 转化成
DO
下userInfo
数据传入到Mapper
层插入到数据库的user_info
表中, 并返回true
。
2. 登录模块
<1>. 约定前后端交互接口
对于登录请求:只需要传入
userName
和password
即可~
如果登录成功, 响应登录成功的
userId
和Token
。
<2>. 梳理实现逻辑
- 首先 用户发送一个登录请求,从
Controller
层传入到Service
层之后
- 其次
Service
层调用通过userName
获取用户信息。
- 然后获取得到的 用户密码和传入的密码都放入加密单元验证 。
- 最后如果验证成功就通过
JWT
单元 构造Token
信息响应给前端 , 如果验证失败就抛出登录失败的异常!
七. 验证码模块
<1>. 约定前后端交互接口
传入一个 验证码信息 即可~
响应一个
true
表示验证通过,false
表示未通过。
<2>. 梳理实现逻辑
由于验证码的逻辑比较简单, 直接在 Controller
层实现所有的逻辑
如时序图:
- 首先, 用户发送一个验证码请求, 达到
Controller
层之后先判空,是否输入为是空串。
- 其次,从
session
从获取载荷信息, 包括验证码
和当时时间
- 然后先判断当时时间是否超过进入的一分钟, 如果超过,验证失效直接返回
false
- 在有效时间内, 返回生成的验证码与输入验证码的比对结果。
八. 用户模块
用户模块包括 登录模块与注册模块
但是除了这两个模块之外,还有获取
用户信息
和作者信息
1.获取用户信息
<1>. 约定前后端交互接口
请求参数通过 查询字符串 的方式传递
响应给数据为:用户
id
, 用户名userName
,图片路径picture
, github地址githubUrl
, 以及 总篇数sum
, 总人数count
。
<2>. 梳理实现逻辑
- 首先,前端传入一个
userId
, 在Controller
层先判空传入Service
层
- 然后
Service
层 根据获取得到的userId
传入到Mapper
层到达MySQL库表
中查询, 注意这里需要 判断一下查询到的用户是否为空,如果用户不存在就直接返回null
- 最后从
Mapper
层中查询得到 用户信息,文章总数 和 总人数 整合响应给前端。
2. 获取作者信息
<1>. 约定前后端交互接口
请求参数 通过查询字符串的方式传入一个
blogId
后端响应 当前
blogId
下的用户的id
,userName
,picture
,githubUrl
,sum
,count
这些属性。
<2>. 梳理实现逻辑
- 首先传入一个
blogId
给服务层, 服务层调用Mapper
层的 博客表查询
- 其次查询出的博客信息,先判空是否存在, 然后获取博客信息下的
userId
- 最后将
userId
直接调用Service
层的 获取用户信息 的方法响应用户信息即可。
九. 博客模块
博客模块的核心主要是:
博客列表模块, 博客详情模块,添加博客模块
,更新博客模块, 删除博客模块。
1. 博客列表模块
<1>. 约定前后端交互接口
前端发送 当前页数
currentPage
和 单页条数pageSize
两个参数
后端响应数据: 总页数
total
,博客总记录records
: 包括id
,userId
,title
,content
,updateTime
, 以及 请求参数request
: 包括currentPage
,pageSize
以及offset
。
<2>. 梳理实现逻辑
如上方时序图:
- 前段发送页数等参数,穿入后端
Service
层 , 进入Mapper
层查询blog_info
表
- 接 着在
Mapper
层,先获取博客总记录数 , 然后进行 分页查询获取记录数。
- 最后 整合返回数据,进行响应。
2. 博客详情模块
<1>. 约定前后端交互接口
请求参数通过查询字符串,以
blogId
定位博客
响应获取数据为博客详情信息: 博客:
id
, 用户 :userId
, 标题 :title
, 正文 :content
, 更新时间:updateTime
。
<2>. 梳理实现逻辑
- 首先前端发送
博客id
给后端, 后端服务层接收到参数之后进入Mapper层
查询
- 其次进入到
Mapper
层 ,达到数据库blog_info
表中查询
- 最终
DO
层数据整合成Response
层数据响应。
3. 添加博客模块
<1>. 约定前后端交互接口
编辑博客后,前端传入博客的基本信息: 用户:userId
, 标题 : title
, 正文 : content
。
响应数据: true
表示 创建成功,false
表示 创建失败 。
<2>. 梳理实现逻辑
- 首先,前端传入 博客数据, 到达
Service
层, 并将传入参数进行类型转化
- 最终转化成
DO层
数据之后, 进入Mapper层
达到MySQL
的blog_info
。
- 判读数据 是否插入成功 , 成功效应
true
, 不成功抛出异常和打印错误信息。
4. 更新博客模块
<1>. 约定前后端交互接口
更新文章的请求参数,本质上和添加文章的请求参数一模一样,但是 需要注意的是这里的
id
是博客id
, 并不是用户id
。
响应这里就不赘述了~
<2>. 梳理实现逻辑
如上时序图:
-
首先, 前端插入需要更新的数据, 达到Service层后先进行转换成DO层类型数据。
-
然后进入
Mapper
层达到MySQL
的blog_info
表中 更新数据。 -
最终更新成功返回
true
。
5. 删除博客模块
<1>. 约定前后端交互接口
请求参数通过 查询字符串 的方式传入
blogId
表示需要删除当前blogId
响应返回一个布尔类型,
true
表示 删除成功, 反之失败。
<2>. 梳理实现逻辑
如时序图:
- 首先请求参数传入到
Service
层,首先设置删除标志为1
,
- 然后进入
Mapper
层删除, 将目标博客设置为1
进行 逻辑删除 。
- 最后如果判断是否删除成功,返回
true
, 否则false
。
十. 源码展示
小编对于核心的模块已经介绍完毕了, 还有一些小编认为不是主要的内容, 小伙伴可以 参考源码 学习哦~
以上就是 《博客系统》
的全部内容哦~
如果觉得小编写的还不错的咱可支持 三连 下 (定有回访哦) , 不妥当的咱请评论区 指正
希望我的文章能给各位宝子们带来哪怕一点点的收获就是 小编创作 的最大 动力 💖 💖 💖