Spring Boot电脑商城项目
收获的东西
1、使用 md5 进行注册时密码的加密及登录时密码判断操作
/**
* 用户模块业务层的实现类
*/
@Override
public void reg(User user) {
//通过user参数来获取传递过来的username
String username = user.getUsername();
//调用mapper的findByUsername(username)判断用户是否被注册过了
User result = userMapper.findByUsername(username);
//判断结果集是否为null,不为null的话则需抛出用户名被占用的异常
if (result != null) {
// 抛出异常
throw new UsernameDuplicatedException("'用户名被占用,请换个用户名");
}
// 密码加密处理的实现:md5算法的形式:67sssss-67sssss-67sssss-67sssss
// 串 + password + 串 ---- md5算法进行加密,连续加载三次
// 盐值 + password + 盐值 ---- 盐值就是一个随机的字符串
String oldPassword = user.getPassword();
// 获取盐值(随机生成字符串)
String salt = UUID.randomUUID().toString().toUpperCase();
// 补全数据:盐值的记录
user.setSalt(salt);
// 将密码和盐值作为一个整体作为加密处理,忽略原有密码强度提升了数据的安全性
String md5Password = getMD5Password(oldPassword, salt);
// 将加密之后的密码重新补全设置到 user 对象中
user.setPassword(md5Password);
//补全数据:is_delete设置为0
user.setIsDelete(0);
//补全数据:四个日志字段信息
user.setCreatedUser(user.getUsername());
user.setModifiedUser(user.getUsername());
Date date = new Date();
user.setCreatedTime(date);
user.setModifiedTime(date);
// 执行注册业务层功能的实现
Integer rows = userMapper.insert(user);
if (rows != 1) {
throw new InsertException("在用户注册过程中产生未知错误");
}
}
@Override
public User login(String username, String password) {
//根据用户名称来查询用户的数据是否存在,不存在则抛出异常
User result = userMapper.findByUsername(username);
if (result == null) {
throw new UserNotFoundException("用户数据不存在");
}
/**
* 检测用户的密码是否匹配:
* 1.先获取数据库中加密之后的密码
* 2.和用户传递过来的密码进行比较
* 2.1先获取盐值,注册时自动生成的盐值
* 2.2将获取的用户密码按照相同的md5算法加密
*/
String oldPassword = result.getPassword();
String salt = result.getSalt();
String newMd5Password = getMD5Password(password, salt);
if (!newMd5Password.equals(oldPassword)) {
throw new PasswordNotMatchException("用户密码错误");
}
//判断is_delete字段的值是否为1,为1表示被标记为删除
if (result.getIsDelete() == 1) {
throw new UserNotFoundException("用户数据不存在");
}
//方法login返回的用户数据是为了辅助其他页面做数据展示使用(只会用到uid,username,avatar)
/*
所以可以new一个新的user只赋这三个变量的值,这样使层与层之间传输时数据体量变小,后台层与
层之间传输时数据量越小性能越高,前端也是的,数据量小了前端响应速度就变快了
*/
User user = new User();
user.setUid(result.getUid());
user.setUsername(result.getUsername());
user.setAvatar(result.getAvatar());
return user;
}
/**
* 定义一个md5算法的加密处理
*/
private String getMD5Password(String password, String salt) {
// md5加密算法方法的调用(进行三次加密)
for (int i = 0; i < 3; i++) {
password = DigestUtils.md5DigestAsHex((salt + password + salt).getBytes()).toUpperCase();
}
// 返回加密之后的密码
return password;
}
2、拦截器 和 过滤器【之前模糊】
2.1、定义用户登录拦截器
源码:
public interface HandlerInterceptor {
// 在调用所有处理请求的方法之前被自动调用执行的方法
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
// 在 ModelAndView 对象返回之后被调用的方法
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
// 在整个请求所有关联的资源被执行完毕最后所执行的方法
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}
实现,创建在interceptor包下
/**
* 定义一个登录拦截器
*/
public class LoginInterceptor implements HandlerInterceptor {
/**
* 监测全局 session 对象中是否有 uid数据,如果有则放行,如果没有重定向到登录界面
* @param request 请求对象
* @param response 响应对象
* @param handler 处理器(url + Controller: 映射)
* @return 如果返回值为true表示放行当前的请求,如果返回值为false则表示拦截当前的请求
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// HttpServletRequest 对象来获取session对象
Object obj = request.getSession().getAttribute("uid");
if (obj == null) {
// 说明用户没有登录过系统,则重定向到 login.html页面
response.sendRedirect("/web/login.html");
// 结束后续的调用
return false;
}
// 请求放行
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
2.2、注册过滤器
创建在 config 包下
/**
* 处理器拦截器的注册
*/
@Configuration // 加载当前的拦截器并进行注册
public class LoginInterceptorConfigurer implements WebMvcConfigurer {
/**
* 配置拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 创建自定义拦截器对象
HandlerInterceptor interceptor = new LoginInterceptor();
// 配置白名单:存放在一个 list集合
List<String> patterns = new ArrayList<>();
patterns.add("/bootstrap3/**");
patterns.add("/css/**");
patterns.add("/image/**");
patterns.add("/js/**");
patterns.add("/web/register.html");
patterns.add("/web/login.html");
patterns.add("/web/index.html");
patterns.add("/web/product.html");
patterns.add("/users/reg");
patterns.add("/users/login");
// 完成拦截器的注册
registry.addInterceptor(interceptor)
.addPathPatterns("/**") // 表示要拦截的url是什么
.excludePathPatterns(patterns);
}
}
如果提示重定向次数过多,login.html页面无法打开。将浏览器cookie请求清空,再将浏览器设置为初始设置。
3、用户上传头像 【没有解决指定路径问题】
错误方法:把文件存到数据库中,需要图片时访问数据库,数据库将文件解析为字节流返回,最后写到本地的某一个文件.这种方法太耗费资源和时间了
正确方法:将对应的文件保存在操作系统上,然后再把这个文件路径记录下来,因为在记录路径的时候是非常便捷和方便的,将来如果要打开这个文件可以依据这个路径找到这个文件,所以说在数据库中保存该文件的路径即可.
稍微大一点的公司都会将所有的静态资源(图片,文件,其他资源文件)放到某台电脑上,再把这台电脑作为一台单独的服务器使用
3.1、上传头像-持久层
- SQL语句规划
对应一个更新用户 avatar 字段的 sql 语句
update t_user set avatar=?,modified_user=?,modified_time=? where uid=?
3.2、设计接口和抽象方法
在UserMapper接口中定义一个抽象方法用于修改用户的头像
/**
* 注解@Param("SQL映射文件中#{}占位符的变量名"),解决的问题:
* 当SQL语句的占位符和映射的接口方法参数名不一致时,需要将某个参数强行注入到某个
* 占位符变量上时,可以使用@Param这个注解来标注映射的关系
* 根据用户uid修改用户的头像
* @param uid 用户id
* @param avatar 头像地址
* @param modifiedUser 修改者
* @param modifiedTime 修改时间
* @return 返回受影响行数
*/
Integer updateAvatarByUid(@Param("uid") Integer uid,//@Param("参数名")注解中的参数名需要和sql语句中
//的#{参数名}的参数名保持一致.该处表示iddddd中的变量值要注入到sql语句的uid中
@Param("avatar") String avatar,
@Param("modifiedUser") String modifiedUser,
@Param("modifiedTime") Date modifiedTime);
3.3、接口的映射
<update id="updateAvatarByUid">
UPDATE store.t_user
SET
avatar=#{avatar},
modified_user=#{modifiedUser},
modified_time=#{modifiedTime}
WHERE
uid=#{uid}
</update>
在测试类中编写测试方法:也可使用 Postman
@Test
public void updateAvatarByUid() {
userMapper.updateAvatarByUid(6, "/upload/avatar.png", "管理员", new Date());
}
3.4、上传头像-业务层
- 规划异常
- 用户数据不存在,找不到对应的用户数据
- 更新的时候,出现未知异常
- 设计接口和抽象方法
/**
* 修改用户头像
* @param uid 用户id
* @param avatar 用户头像路径
* @param username 用户名称
*/
void changeAvatar(Integer uid, String avatar, String username);
- 实现抽象方法
@Override
public void changeAvatar(Integer uid, String avatar, String username) {
User result = userMapper.findByUid(uid);
if (result == null || result.getIsDelete() == 1) {
throw new UserNotFoundException("用户数据不存在");
}
Integer rows = userMapper.updateAvatarByUid(uid, avatar, username, new Date());
if (rows != 1) {
throw new UpdateException("更新用户头像产生未知异常");
}
}
测试业务层方法
@Test
public void updateAvatarByUid() {
userService.changeAvatar(6, "/upload/test.png", "管理员");
}
3.5、上传头像-控制层
- 规划异常
文件异常的父类
FileUploadException 泛指文件上传异常(父类) 继承 RuntimeException
FileEmptyException 文件为空的异常
FileStateException 文件上传状态异常
FileSizeException 文件大小超出限制
FIleTypeException 文件类型异常
FileUploadIOException 文件读写异常
五个构造方法显示声明出来,再去继承相关的父类
- 处理异常
在基类 BaseController 类中进行编写和统一处理
else if (e instanceof FileEmptyException) {
result.setState(6000);
result.setMessage("文件上传为空的异常");
} else if (e instanceof FileSizeException) {
result.setState(6001);
result.setMessage("文件大小超出限制异常");
}else if (e instanceof FileTypeException) {
result.setState(6002);
result.setMessage("文件类型异常");
} else if (e instanceof FileStateException) {
result.setState(6003);
result.setMessage("文件上传状态异常");
} else if (e instanceof FileUploadIOException) {
result.setState(6004);
result.setMessage("文件读写异常");
}
在异常统一处理方法的参数列表上增加新的异常处理作为它的参数
@ExceptionHandler({ServiceException.class, FileUploadException.class}) // 用于统一处理抛出的异常
-
设计请求
- /users/change_avatar
- POST(GET请求提交数据只有2KB左右)
- HttpSession session(获取uid和username),MultipartFile file
- JsonResult(不能是JsonResult:如果上传头像后浏览别的页面,然后再回到上传头像的页面就展示不出来了,所以图片一旦上传成功,就要保存该图片在服务器的哪个位置,这样的话一旦检测到进入上传头像的页面就可以通过保存的路径拿到图片,最后展示在页面上)
-
实现
/**
* 设置上传文件的最大值
*/
public static final int AVATAR_MAX_SIZE = 10 * 1024 * 1024;
/**
* 限制上传文件的类型
*/
public static final List<String> AVATAR_TYPE = new ArrayList<>();
static {
AVATAR_TYPE.add("images/jpeg");
AVATAR_TYPE.add("images/png");
AVATAR_TYPE.add("images/bmp");
AVATAR_TYPE.add("images/gif");
}
/**
* MultipartFile 接口 是 SpringMVC 提供的一个接口,这个接口为我们包装了
* 获取文件类型的数据(任何类型的file都可以接收),SpringBoot 它又整合了
* SpringMVC,只需要在处理请求的方法参数列表声明一个参数类型为 MultipartFile
* 的参数,然后 SpringBoot 自动将传递给服务的文件数据赋值给这个参数
*
* <input type=
* "file" name="file">中的name="file",所以必须有一个方法的参数名
* 为file用于接收前端传递的该文件.如果想要参数名和前端的name不一
* 样:@RequestParam("file") MultipartFile file:把表单中name=
* "file"的控件值传递到变量file上
*
* @param session
* @param file
* @return
*/
@RequestMapping("change_avatar")
public JsonResult<String> changeAvatar(HttpSession session, @RequestParam("file") MultipartFile file) {
// 判断文件是否为 null
if (file.isEmpty()) {
throw new FileEmptyException("文件为空");
}
// 判断文件大小
if (file.getSize() > AVATAR_MAX_SIZE) {
throw new FileSizeException("文件大小超出限制");
}
// 判断文件类型是否是我们规定的后缀类型
String contentType = file.getContentType();
// 如果集合包含某个元素则返回 true
if (!AVATAR_TYPE.contains(contentType)) {
throw new FileTypeException("文件类型不支持");
}
// 上传的文件 ../upload/文件名.png
String parent = session.getServletContext().getRealPath("upload");
// File 对象 指向这个路径,File 是否存在
File dir = new File(parent);
if (!dir.exists()) { // 检测目录是否存在
dir.mkdirs(); // 创建当前目录
}
// 获取到这个文件名称, UUID 工具来生成一个新的字符串作为文件名
// 例如: avatar1.png
String originalFilename = file.getOriginalFilename();
System.out.println("originalFilename=" + originalFilename);
int index = originalFilename.lastIndexOf(".");
String suffix = originalFilename.substring(index);
String filename = UUID.randomUUID().toString().toUpperCase() + suffix;
// 创建文件路径
File dest = new File(dir, filename); // 此时是一个空文件
// 将参数 file 中数据写入到这个空文件中
try {
file.transferTo(dest); // 将 参数file文件中的数据传入到 dest文件中
} catch (FileStateException e) {
throw new FileStateException("文件状态异常");
} catch (IOException e) {
throw new FileUploadIOException("文件读写异常");
}
Integer uid = getuidFromSession(session);
String username = getUsernameFromSession(session);
// 返回头像的路径 /upload/test.png
String avatar = "/upload" + filename;
// 调用头像修改
userService.changeAvatar(uid, avatar, username);
// 返回用户头像路径给前端页面,将来用于头像展示使用
return new JsonResult<>(OK, avatar);
}
3.6、上传头像-前端页面
- 头像上传表单要有
<form action="/users/change_avatar" method="post" enctype="multipart/form-data">
3.7、解决Bug
- 更改默认的大小限制
SpringMVC 默认为 1MB 文件可以进行上传,手动的去修改 SpringMVC 默认上传文件的大小
方式一:直接可以在配置文件中进行配置
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=15MB
方式二:需要采用 Java 代码的形式来设置文件的上传大小的限制。主类中配置,可以定义一个方法,必须使用@Bean来修饰。在类的前面添加 @Configuration注解来进行修改类。 MultipartConfigElement
@Configuration // 表示配置类
@SpringBootApplication
@MapperScan("com.cy.store.mapper")
public class StoreApplication {
public static void main(String[] args) {
SpringApplication.run(StoreApplication.class, args);
}
public MultipartConfigElement getMultipartConfigElement() {
// 创建一个配置的工厂类对象
MultipartConfigFactory factory = new MultipartConfigFactory();
// 设置需要创建的对象的相关信息
factory.setMaxFileSize(DataSize.of(10, DataUnit.MEGABYTES));
factory.setMaxRequestSize(DataSize.of(15, DataUnit.MEGABYTES));
// 通过工厂类来创建 MultipartConfigElement 对象
return factory.createMultipartConfig();
}
}
- 显示头像
在页面中通过ajax请求来提交文件,提交完成后返回了json串,解析出json串中的data数据设置到img标签的src属性上
1.serialize():可以将表单数据自动拼接成key=value的结构提交给服务器,一般提交的是普通的控件类型中的数据(type=text/password/radio/checkbox等等)
2.FormData类:将表单中数据保持原有的结构进行数据提交.文件类型的数据可以使用FormData对象进行存储
使用方法:new FormData($(“form”)[0]);
这行代码的含义是将id="form"的表单的第一个元素的整体值作为创建FormData对象的数据
3.虽然我们把文件的数据保护下来了,但是ajax默认处理数据时按照字符串的形式进行处理,以及默认会采用字符串的形式进行数据提交.手动关闭这两个功能:
processData: false,//处理数据的形式,关闭处理数据
contentType: false,//提交数据的形式,关闭默认提交数据的形式
<!--上传头像表单开始-->
<form id="form-change-avatar" class="form-horizontal" role="form">
<div class="form-group">
<label class="col-md-2 control-label">选择头像:</label>
<div class="col-md-5">
<img id="img-avatar" src="../images/index/user.jpg" class="img-responsive" />
</div>
<div class="clearfix"></div>
<div class="col-md-offset-2 col-md-4">
<input type="file" name="file">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<input id="btn-change-avatar" type="button" class="btn btn-primary" value="上传" />
</div>
</div>
</form>
<script type="text/javascript">
$("#btn-change-avatar").click(function () {
$.ajax({
url: "/users/change_avatar",
type: "POST",
data: new FormData($("#form-change-avatar")[0]),
processData: false, // 处理数据的形式,关闭处理数据
contentType: false, // 提交数据的形式,关闭默认提交数据的形式
dataType: "JSON",
success: function (json) {
if (json.state === 200) {
alert("头像修改成功");
// 将服务器端返回的头像地址设置 img 标签的 src 属性上
// attr(属性,属性值)用来给某个属性设值
$("#img-avatar").attr("src", json.data);
} else {
alert("头像修改失败");
}
},
error: function (xhr) {
alert("修改头像时产生未知异常" + xhr.message);
}
});
});
</script>
- 登录后显示头像
将头像上传后会显示头像,但是关闭浏览器后再进入个人头像页面就不会显示头像了,因为只有点击"上传"才能发送ajax请求并显示头像.
可以在每次用户登录成功后将avatar保存在cookie中,登录的业务层返回给控制层user对象,该对象包含uid,username,avatar.所以要在登录页面login.html中将服务器返回的头像路径设置到cookie中,然后每次检测到用户打开上传头像页面,在这个页面中通过ready()方法来自动读取cookie中头像路径并设到src属性上
1、设置cookie的值
(1) 需要在login.html页面头部导入cookie.js文件
(2) 调用cookie方法保存路径
$.cookie(key,value,time);//time单位:天
(3) 在ajax请求原有的代码上加$.cookie(“avatar”,json.data.avatar,{expires: 7});
success: function (json) {
if (json.state == 200) {
location.href = "index.html";
$.cookie("avatar",json.data.avatar,{expires: 7});
} else {
alert("登录失败")
}
},
(4) 然后需要在upload.html获取cookie中的值,所以要在页面头部导入cookie.js文件
(5) 在upload.html的script标签中加ready()自动读取cookie数据
$(document).ready(function () {
let avatar = $.cookie("avatar");
// 将cookie值获取出来设置到头像 src属性上
$("#img-avatar").attr("src", avatar);
})
3.8、显示最新头像
上传头像后不重新登录而是浏览其他页面,然后再进入个人头像页面时展示的头像是上次上传的,因为此时cookie中的值是上次上传的头像的路径,所以需要上传头像后使用同名覆盖更改cookie中路径
在ajax函数的success属性值的if语句加:
$.cookie("avatar",json.data,{expires: 7});
4、yaml文件自定义配置
user.address.max-count=20
/**
* 为了方便日后修改最大收货地址数量,可以在配置文件
* application.properties中定义user.address.max-count=20
*/
//spring读取配置文件中数据:@Value("${user.address.max-count}")
@Value("${user.address.max-count}")
private Integer maxCount;
5、截取路径获取 id
<a href="product.html?id=#{id}">
<!-- 用于截取在 index.html页面中 点击商品,通过 product.html?id=#{id} 来获取 商品的id -->
<script type="text/javascript" src="../js/jquery-getUrlParam.js"></script>
<script type="text/javascript">
//调用jquery-getUrlParam.js文件的getUrlParam方法获取商品id
var id = $.getUrlParam("id");
console.log("id=" + id);
</script>
6、html()方法
//html()方法:
// 假设有个标签<div id="a"></div>
//那么$("#a").html(<p></p>)就是给该div标签加p标签
//$("#a").html("我爱中国")就是给该div标签填充"我爱中国"内容
$("#product-title").html(json.data.title);
7、在ajax函数中 data参数的数据设置的方式
-
data: $(“form表单选择”).serialize()。当参数过多并且在同一个表单中,字符串的提交等
-
data: new FormData($(“form表单选择”)[0])。只适合提交文件
-
data: “username=Tome”。适合参数值固定并且参数列表有限,可以进行手动拼接
let user = "TOM" data: "username=" + user
-
适合 JSON 格式提交数据
data: { "username": "Tome", "age": 18, "sex": 0 }
8、VO
VO全称Value Object,值对象。当进行select查询时,查询的结果属于多张表中的内容,此时发现结果集不能直接使用某个POJO实体类来接收,因为POJO实体类不能包含多表查询出来的信息,解决方式是:重新去构建一个新的对象,这个对象用于存储所查询出来的结果集对应的映射,所以把这个对象称之为值对象.
public class CartVO implements Serializable {
private Integer cid;
private Integer uid;
private Integer pid;
private Long price;
private Integer num;
private String title;
private Long realPrice;
private String image;
}
9、截取地址栏中?后面的数据
data: location.search.substr(1)这个API的参数为1表示截取地址栏中?后面的数据,即参数
如果这个API的参数为0则表示截取地址栏中?前面的数据,即请求地址
10、AOP
检测项目所有业务层方法的耗时(开始执行时间和结束执行时间只差值),再在不改变项目主体流程代码的前提条件下完成此功能,就要用到AOP
如果我们想对业务某一些方法同时添加相同的功能需求,并且在不改变业务功能逻辑的基础之上进行完成,就可以使用AOP的切面编程进行开发
10.1、Spring AOP
AOP:面向切面(Aspect)编程。AOP并不是Spring框架的特性(Spring已经被整合到了SpringBoot中,所以如果AOP是Spring框架的特性,那么就不需要手动导包,只需要在一个类上写@Aspect注解,鼠标放到该注解上按alt+enter就可以自动导包了,但是事与愿违,所以说AOP并不是Spring框架的特性),只是Spring很好的支持了AOP。
- 使用步骤
- 首先定义一个类,将这个类作为切面类
- 在这个类中定义切面方法(5种:前置,后置,环绕,异常,最终)
- 将这个切面方法中的业务逻辑对应的代码进行编写和设计
- 通过连接点来连接目标方法,就是用粗粒度表达式和细粒度表达式来进行连接
10.2、切面方法
1.切面方法的访问权限是public。
2.切面方法的返回值类型可以是void或Object,如果该方法被@Around注解修饰,必须使用Object作为返回值类型,并返回连接点方法的返回值;如果使用的注解是@Before或@After等其他注解时,则自行决定。
3.切面方法的名称可以自定义。
4.切面方法可以接收参数,参数是ProccedingJoinPoint接口类型的参数.但是@Around所修饰的方法必须要传递这个参数.其他注解修饰的方法要不要该参数都可以
10.3、统计业务方法执行时长
1.因为AOP不是Spring内部封装的技术,所以需要进行导包操作:在pom.xml文件中添加两个关于AOP的依赖aspectjweaver和aspectjtools。
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
</dependency>
2.在com.cy.store.aop包下创建TimerAspect切面类,给类添加两个注解进行修饰:
- @Aspect (将当前类标记为切面类)
- @Component (将当前类的对象创建使用维护交由Spring容器维护)
@Aspect
@Component
public class TimerAspect {
}
3.在类中添加切面方法,这里使用环绕通知的方式来进行编写。ProceedingJoinPoint 接口表示连接点,目标方法的对象
public Object around(ProceedingJoinPoint pjp) throws Throwable {
//开始时间
long start = System.currentTimeMillis();
//调用目标方法,比如login方法,getByUid方法
Object result = pjp.proceed();
//结束时间
long end = System.currentTimeMillis();
System.out.println("耗时:"+(end-start));
return result;
}
4.将当前环绕通知映射到某个切面上,也就是指定连接的点.给around方法添加注解@Around
@Around("execution(* com.cy.store.service.impl.*.*(..))")
- 第一个*表示方法返回值是任意的
- 第二个*表示imp包下的类是任意的
- 第三个*表示类里面的方法是任意的
- (…)表示方法的参数是任意的
5.启动项目,在前端浏览器访问任意一个功能模块进行功能的测试