简介
暗星旅游网,是一个分为管理员端和用户端的项目,有权限分离认证,管理员端(后台)进行旅游产品的维护,主要功能有:管理员管理,角色管理,权限管理,认证和授权,产品类型管理,旅游产品管理;用户端(前台)进行旅游产品的展示,主要功能有:用户注册和登录,查询旅游产品,收藏旅游产品。通过开发本项目后,我可以分析并开发常见的门户网站。
项目地址:
注意事项!!!!!
项目涉及路径,必须直接放在D盘之中才能运行
技术选型
JAVA版本:JDK11
数据库:Mysql5.7+Navicat
后端框架:SpringBoot2.7.1 + SpringMVC + Mybatis-Plus3.5.0
权限控制:SpringSecurity
前端框架:AdminLTE2
模板引擎:Thymeleaf 选择原因:Thymeleaf是一个Java库,主要用于在Web和独立应用程序中处理服务器端HTML模板。它是一个开源项目,可以在HTML文件中直接编写Thymeleaf标签。这使得模板更易于阅读和维护,可以轻松地与Spring框架集成。
工具类:发邮件工具类、生成验证码工具类
其他技术:lombok、ajax、logback
数据库方面
构建数据库
详情请移步我的这篇文章
navicat中用sql语言创建数据库_navcat用sql语句新建数据库-CSDN博客文章浏览阅读322次。navicat中用sql语言创建数据库_navcat用sql语句新建数据库https://blog.csdn.net/Black__Emperor/article/details/134747982?spm=1001.2014.3001.5501
数据库表展示
admin-----------------管理员表
admin_role----------管理员和角色的中间表
categony-------------产品类别表
favorite----------------用户的收藏
member---------------用户表
permission------------权限表
product-----------------产品
role----------------------角色表
role_permission------角色和权限的中间表
项目构建
创建
在idea中构建一个springboot项目
添加依赖
在xml中进行配置和文件导入,等待maven中下载
等待期间不会太久,取决于网速,耐心等待就行了
项目搭建_AdminLTE
项目分为管理员端和用户端。管理员端负责管理网站资源,发布旅游产品;用户端可以查询旅游产品,收藏旅游产品等。两端使用的页面风格不同。在项目中,我们使用AdminLTE框架作为管理员端页面,使用自己编写的网页作为用户端页面。AdminLTE是一款建立在bootstrap和jquery之上的开源的模板主题工具,它提供了一系列响应的、 可重复使用的组件,并内置了多个模板页面;同时自适应多种屏幕分辨率,兼容PC和移动端。通过AdminLTE,我们可以快速的创建一个响应式的Html5网站。AdminLTE框架在网页架构与设计上,有很大的辅助作用,尤其是前端架构设计师,用好AdminLTE 不但美观,而且可以免去写很大CSS与JS的工作量。
使用AdminLTE非常简单,只需要根据需求将需要的组件复制到我们的页面中即可。
处理页面
把其中有用的页面复制,挪到自己的项目中。
项目搭建_编写后台首页
接下来我们使用AdminLTE的模板页面编写后台首页index.html
项目搭建_提取统一后台模板
提取common_header.html
后台用户管理_管理员列表
后台用户也称为管理员,每个管理员能在后台进行的操作不同,所以不同的管理员有不同的权限。在项目中,权限表的设计为用户—角色
多对多,角色—权限
多对多,既一个用户有多个角色,一个角色有多个权限。所以后台首先要拥有用户管理、角色管理、权限管理的功能。
后端处理
配置数据库所需的...类,将数据库里面的在后端也写出来
配置一下主类和启动页面
启动成功
-------------------- 省略一部分调试的过程 ------------------------
前端
前端鉴权配置:在用户访问中,对于没有权限的页面,仍然能够点击按钮来跳转到一个403页面,这对于用户来说是极其不友好的,对于程序员来说,需要在前端加上鉴权配置,对于不合乎规范的没权限用户,直接不给他显示按钮界面。
方案:在开发中,用户如果没有权限,我们往往会将页面上的内容隐藏。此时需要Thymeleaf
整合Spring Security
进行前端鉴权。修改common_aside.html
,添加thymeleaf-springsecurity
命名空间
项目内展示
更新过后,无权限直接失去对应的显示功能
-------------对新用户授权的一个测试---------------
密码加密说明
在我们修改的时候,密码是返回回来的,而保存还是要提交密码的,在我们保存的时候返回了的密码是加密过后的密码了,那么只要不修改密码就不能对他进行加密,否则得到一个二次加密的密码,(你小子甜蜜的以为自己在二战是吧搞这么结实)
因此,在这里去拿一下他的旧密码和新密码,防止出现套娃密码
// 修改管理员
public void update(Admin admin) {
// 旧密码
String oldPassword = adminMapper.selectById(admin.getAid()).getPassword();
// 新密码
String newPassword = admin.getPassword();
// 如果新密码不等于旧密码,对新密码进行加密
if (!oldPassword.equals(newPassword)){
admin.setPassword(encoder.encode(newPassword));
}
adminMapper.updateById(admin);
}
创建一个新用户对她进行权限分配
可以看到,1026号就有一个恶臭管理员了
并且恶臭管理员还具有查询权限(提前设置了的)
-------------对旅游产品的一个增删改查---------------
@Controller
@RequestMapping("/backstage/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
@RequestMapping("/all")
public ModelAndView all(@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10")int size){
Page<Category> categoryPage = categoryService.findPage(page, size);
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("categoryPage",categoryPage);
modelAndView.setViewName("/backstage/category_all");
return modelAndView;
}
@RequestMapping("/add")
public String add(Category category){
categoryService.add(category);
return "redirect:/backstage/category/all";
}
@RequestMapping("/edit")
public ModelAndView edit(Integer cid){
Category category = categoryService.findById(cid);
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("category",category);
modelAndView.setViewName("/backstage/category_edit");
return modelAndView;
}
@RequestMapping("/update")
public String update(Category category){
categoryService.update(category);
return "redirect:/backstage/category/all";
}
@RequestMapping("/delete")
public String delete(Integer cid){
categoryService.delete(cid);
return "redirect:/backstage/category/all";
}
}
再完善三个页面,增加,显示,修改的页面
已经可以看到产品的信息了
试一下新增的方法是否可行
很明显,正常载入,不过没有采用软删除,而是直接删除(所谓软删除指的是在数据库中有isdel列,如果为0则正常显示,不为0就无法显示,和上方的管理员那里的屏蔽管理员如出一辙)
旅游产品的初步显示(管理员端)
后台
在显示产品的时候,需要进行多表查询,就是Product表关联category表,所以这边我们就不能使用mybatisplus原生的方法来进行分页查询了。他原生的方法来进行分页查询只能单表查询 ,所以需要自定义SQL语句来多表查询
对于页数,因为这里会去显示产品的图片,因此,为了让占地方的图片不被压缩,这里把一个页面上的显示改为5条
前台
发现没有显示我们的产品图片,按F12,查看控制台
嗷~~原来是我的产品图片路径不对,没放在这个包里面,然后他就找不到404报错了
把图片素材掏进来
还是没有,去翻一下数据库
这是我们的地址,但是似乎放的位置不太对,我们再来调整一下
........
很好,是自己眼拙了,upload包应该是static的子包,和backstage同级,但是我手误还是什么,给创建在backstage里面了...
-----创建新增产品-----
试一下新增
保存成功。待会弄图片
富文本编辑器
-----------后台产品管理 富文本编辑器------------------
在编写产品详情时,往往需要加入一些文字样式或者插入图片,这样最终给用户展示出来的效果会更好。那么,这个时候要想在插入内容时拥有样式,需要使用富文本编辑器wangEditor
如图,没法设置字体
官网地址:wangEditor
-
在前端页面中引入
wangEditor.js
-
在页面中加入
wangEditor
插件
虽然吧,wangEditor出到了第五代,但是,考虑个人审美什么的,我还是义无反顾的拿第四代来放在本次的旅游网项目,当然,老版本更稳定也是一个问题。
什么?你问我,为什么不使用更老的v3版本?
哦莫,V3没了(悲)
咳咳,开个玩笑,V3,启动!
他只是停止维护了,不是不能用了。
把他下载好,弄到 js 里面
引用wangEditor.js
需要注意,我用的AdminLet框架是自带富文本编辑器,他会和wangEditor产生冲突
把他噶掉
用法:直接复制拿过来就行了
替换完毕,这只是有了他们的样子,还需要对他进行修改,来匹配我们的项目
ok,已经有了这个编辑器了,非常完美,看起来
需要注意,wangEditor 从 v3 版本开始不支持 textarea,但是可以通过 onchange 来实现 textarea 中提交富文本内容
我的意见是把自己用的顺手的小组件单独存起来,包括文本说明,这玩意说不定哪天更新就没了
div中的内容可以同步更新到文本域,因此我们可以把文本域中的内容提交到后台
富文本编辑器的本质是在你操作的时候,自动在两侧甲标签,来达到实时调整的效果
--------------------上传图片-----------------
完啦!
上传图片有误,没办法,开始修理
首先来这里看看是怎么上传的
需要编写上传控制器来接受富文本编辑器传出来的图片
然后是一些配置文件
在application.yml里面,把这个配置一下,让我们上传非常大的图片没问题
咳咳,需要注意,图片不能有特殊字符,@#¥%这种,只能是数字,英文,中文,其他符号嘛,别手贱,改一改。(呜呜呜)因为你用的上传图片是前端的一个框架,它底层写的时候是不识别这些的-
后台调整
后台产品管理_修改产品
增加一下查询
随后在Controller里面和Service里面加方法
修改完后端,修改前端,写一个写的html页面,复制一部分共有
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>修改旅游产品</title>
<th:block th:replace="/backstage/common_resources::common_css"></th:block>
<th:block th:replace="/backstage/common_resources::common_js"></th:block>
</head>
<body class="hold-transition skin-purple sidebar-mini">
<header th:replace="~{/backstage/common_header::header}"></header>
<aside th:replace="~{/backstage/common_aside::aside}"></aside>
<div class="wrapper">
<div class="content-wrapper">
<!-- 内容头部 -->
<section class="content-header">
<h1>
旅游产品管理
<small>修改旅游产品</small>
</h1>
<ol class="breadcrumb">
<li><a href="@{/backstage/index}"><i class="fa fa-dashboard"></i> 首页</a></li>
<li><a href="@{/backstage/product/all}">旅游产品管理</a></li>
<li class="active">修改旅游产品</li>
</ol>
</section>
<!-- 内容头部 /-->
<!-- 正文区域 -->
<section class="content">
<div class="row data-type">
<div class="col-md-2 title" style="height: 110px">产品图片</div>
<div class="col-md-8 data" style="height: 110px">
<form id="uploadPImage" enctype="multipart/form-data">
<input type="file" name="file" id="pImageFile">
</form>
</div>
<script>
$(function (){
$("#pImageFile").change(function (){
// 异步提交表单
$("#uploadPImage").ajaxSubmit({
url:"/backstage/product/upload",
type: "post",
success:function (result){
// 上传成功后,图片回显到pImage上
$("#pImage").attr("src",result.data[0]);
// 上传成功后,图片地址放入产品表单的隐藏域中
$("#hiddenPImage").val(result.data[0]);
}
})
})
})
</script>
<div class="col-md-2 data" style="height: 110px">
<img height="100" th:src="${product.pImage}" id="pImage">
</div>
<form th:action="@{/backstage/product/update}" method="post">
<input type="hidden" name="pImage" id="hiddenPImage">
<input type="hidden" name="pid" th:value="${product.pid}">
<div class="col-md-2 title">产品名称</div>
<div class="col-md-4 data">
<input type="text" class="form-control" th:value="${product.productName}" name="productName">
</div>
<div class="col-md-2 title">产品类型</div>
<div class="col-md-4 data">
<select class="form-control" name="cid">
<option th:each="category:${categoryList}"
th:value="${category.cid}"
th:text="${category.cname}"
th:selected="${category.cid eq product.cid}"></option>
</select>
</div>
<div class="col-md-2 title">价格</div>
<div class="col-md-4 data">
<input type="number" class="form-control" th:value="${product.price}" name="price">
</div>
<div class="col-md-2 title">热线电话</div>
<div class="col-md-4 data">
<input type="text" class="form-control" th:value="${product.hotline}" name="hotline" value="">
</div>
<div class="col-md-2 title">状态</div>
<div class="col-md-4 data">
<select class="form-control" name="status">
<option value="true">开启</option>
<option value="false">关闭</option>
</select>
</div>
<div class="col-md-6 data"></div>
<div class="col-md-2 title" style="height: 350px">产品详情</div>
<div class="col-md-10 data" style="height: 350px">
<div id="div1" th:utext="${product.productDesc}"></div>
<textarea id="text1" name="productDesc" style="width:100%; height:200px;" hidden></textarea>
<script>
var E = window.wangEditor
var editor = new E('#div1')
var $text1 = $('#text1')
editor.customConfig.onchange = function (html) {
// 监控变化,同步更新到 textarea
$text1.val(html)
}
// 自定义菜单配置
editor.customConfig.menus = [
'head', // 标题
'bold', // 粗体
'fontSize', // 字号
'fontName', // 字体
'italic', // 斜体
'underline', // 下划线
'foreColor', // 文字颜色
'backColor', // 背景颜色
'justify', // 对齐方式
'image', // 插入图片
'undo', // 撤销
]
// 配置上传图片服务器端地址
editor.customConfig.uploadImgServer = '/backstage/product/upload';
// 配置上传图片的参数名
editor.customConfig.uploadFileName = 'file';
editor.create()
// 初始化 textarea 的值
$text1.val(editor.txt.html())
</script>
</div>
<div class="col-md-2 title"></div>
<div class="col-md-10 data text-center">
<button type="submit" class="btn bg-maroon">保存</button>
<button type="button" class="btn bg-default" onclick="history.back(-1);">返回</button>
</div>
</form>
</div>
</section>
<!-- 正文区域 /-->
</div>
</div>
<footer th:replace="~{/backstage/common_footer::footer}"></footer>
</body>
</html>
图片出现不显示
调试bug,观察控制台
遂发现是漏了该行,没导入jq文件导致没发上传请求
完毕
后台代码优化_配置事务
为了避免在项目运行过程中,代码出现异常导致数据错误。我们需要在项目的服务层配置事务。事务即一段代码要么同时成功,要么同时失败。比如在给角色分配权限时发生异常:
public void updatePermissions(Integer rid,Integer[] ids){
// 删除角色的所有权限
roleMapper.deleteRoleAllPermission(rid);
int i = 1/0;
// 重新给角色添加权限
for (Integer pid:ids){
roleMapper.addRolePermission(rid,pid);
}
}
此时如果不给updatePermissions方法添加事务,则在删除角色权限后由于异常导致后面的代码不能执行,此时角色失去原有的所有的权限。我们希望发生异常后,整个方法完成回滚,即删除操作也不执行。
也就是我们搭积木,打了一半,你出现问题,你希望只有最上面有问题的垮了,而不是连带着之前搭好了的一起垮了
SpringBoot默认开启@Transactional
注解,Spring容器会自动扫描@Transactional
修饰的方法和类。当注解在类上的时候意味着此类的所有public
方法都是开启事务的。被注解的方法都成为一个事务整体,同一个事务内共享一个数据库连接,所有操作同时发生。如果在事务内部执行过程中发生了异常,则事务整体会自动回滚。
解决方案:在service
层的所有类上方添加@Transactional
注解即可
图片保存到代码里面的代价就是必须重构,才能加载静态文件
之前可以是,之前是保存在启动的时候程序创建出来的一个tomcat暂存的文件夹,但我的狗屎电脑每次重启给删了,就....呃啊啊啊啊啊啊
演示如下
上传图片后,重启项目
og
图片显示
后台代码优化_记录日志
在方面可以追踪内部人员的操作记录。
SpringBoot默认使用Logback组
运用的思想是AOP
后台代码运行的过程中,我们要对每一次操作进行日志记录,一方面通过日志可以发现代码的缺陷,另一件作为日志管理,首先在/resources
下添加Logback配置文件logback.xml
面向切面编程
以所有的controller层作为切点,在方法执行完成后自动执行打印日志的代码
@Data
public class Log {
private String url; // 访问的路径
private Date visitTime; // 访问时间
private String username; // 访问者
private String ip; // 访问ip
private int executionTime; // 访问时长
private String exceptionMessage; // 异常信息
}
编写日志实体类,这样可以算出用户单次访问的时间,这样可以极大程度的看出用户的体验如何。毕竟我是一个用户至上的良心网站,还是需要考虑一下上帝的😀
dyi
编写切点和通知:定义一个切点,它把所有controller层的方法都作为一个切点
//编写异常通知
@AfterThrowing(pointcut = "pointCut()",throwing = "ex")
public void afterThrowing(Throwable ex){
Log log = new Log();
运行程序
发现日志
打开
这个INFO就有了
username=ANX, ip=0:0:0:0:0:0:0:1, executionTime=5, exceptionMessage=null
用户名,本机,访问时间花费5毫秒,没有发生异常
(记住了啊,千万别把用户名设置为NULL,不然你会被我狠狠的打一顿)
调用一下错误的方法,查看日志
异常日志也存在
这样记录后台日志功能就实现了
用户端前台调整
前台用户注册_网站首页
我们使用自己编写的网页作为用户端页面
上面的frontdesk放了前台静态资源 下面的frontdesk放了前台页面
页脚--footer.html
页头--header.html
首页--index.html
登陆页面--login.html
我的收藏--my_favorite.html
注册页面--register.html
注册成功--register_ok.html
产品详情--route_detail.html
产品列表--route_list.html
前台用户注册_编写注册页面
注册路径 :@{/frontdesk/member/register}
提交的方式:post
这里的密码为了方便可视,目前是text文本,后续改成password保证安全性
前台用户注册_生成验证码
验证码的作用是验证操作者是否是真人,避免机器操作恶意提交。它是后台随机生成的一串字符串,但我们不能将该字符串直接传到前台,否则机器直接读到字符串,验证码将没有任何意义。一般在后台生成验证码后,一方面将验证码保存到session中,另一方面将验证码做成一张图片,将图片传到前台。用户认出验证码后,输入验证码传到后台,如果正确即可判断操作者为真人。
@WebServlet("/frontdesk/checkCode")
会导致浏览器缓存,从而在点击验证码图片不会更换验证码图片
以此,在下方加入3行来阻止服务器缓存
设置验证码的随机数个数和值
@ServletComponentScan 是 SpringBoot启动时扫描注册注解标注的Web组件
如果我们想使用原生的Servlet 包括过滤器间谍性的一定要加这个注解,否则扫描不到就不会有用
<tr>
<td class="td_left">
<label for="check">验证码</label>
</td>
<td class="td_right check">
<input type="text" id="check" name="checkCode" class="check">
<img src="/frontdesk/checkCode" height="32px" alt="" onclick="changeCheckCode(this)">
<script type="text/javascript">
//图片点击事件
function changeCheckCode(img) {
// 在方法后添加参数的原因是如果不添加参数,img.src的属性不会改变,此时浏览器就不会向后端发送请求。
img.src = "/frontdesk/checkCode?" + new Date().getTime();
}
</script>
</td>
</tr>
修改一下注册页面的代码
前台用户注册_编写注册方法
为了保证用户注册的信息是真实的,往往在用户注册后不能直接登录,而需要用户激活后才能登录。用户注册激活的步骤如下:
-
用户在页面填写个人信息,发送到后端代码。
-
后端验证数据后保存用户信息,但此时用户的状态为false,还不能登录。
-
后端拿到用户输入的邮箱,给用户邮箱发送一段随机字符串,并将该字符串保存到数据库的用户表中。
-
用户登录个人邮箱,点击随机字符串访问项目,项目将该拥有字符串的用户状态变为true,此时用户即可登录。
大致如此,以此防止有人大量创建用户,来攻击数据库,造成大量数据给服务器带来卡顿
-
用户在页面填写个人信息,发送到后端代码。
-
后端验证数据后保存用户信息,但此时用户的状态为false,还不能登录。
-
后端拿到用户输入的邮箱,给用户邮箱发送一段随机字符串,并将该字符串保存到数据库的用户表中。
-
用户登录个人邮箱,点击随机字符串访问项目,项目将该拥有字符串的用户状态变为true,此时用户即可登录。
由于注册方法结果很多,我们注册方法需要返回的是否注册成功,如果失败需要返回失败原因。我们要在bean
目录下创建一个实体类Result
,该实体类可以封装返回的数据。
package ANX.travel.bean;
import lombok.AllArgsConstructor;
import lombok.Data;
// 结果对象
@Data
@AllArgsConstructor
public class Result {
private boolean flag;// 结果
private String message;//提示信息
private Object data;//返回数据
public Result(boolean flag, String message) {
this(flag,message,null);
}
}
提示:打断点,进来追踪一下数据
效果展示:
稍后把这个注册成功的页面修复一下下
前台用户注册_发送邮件配置
在用户注册成功后,要向用户的邮箱发送一封激活邮件,发送邮件需要在系统中配置发件人,同学们使用自己的邮箱作为发件人即可。
-
配置邮箱第三方登录。
我们在系统中使用邮箱发送邮件属于第三方登录,而市面上的邮箱默认是不能第三方登录的。我们需要登录邮箱,配置第三方登录。
这里牺牲一下下某人的QQ邮箱 3084260473@qq.com
首先,登陆自己的qq邮箱
点击设置
再点击账户,下拉,找到这里
这里需要自己手机给官方发送短信,得到密码,开启第三方...巴拉巴拉的
最后申请下来一个16位的密钥
配置在这里,user:qq邮箱 password: 16位密钥
可别指望拿我的qq邮箱去耍啊
前台用户注册_发送激活邮件
接下来我们修改注册方法,注册成功后,发送激活邮件:
我们打开F12控制台,点击这个激活,对比数据库中的激活码。
发现是一模一样的,说明成功了
前台用户注册_激活用户
激活方法即拿到激活码,在数据库中根据激活码找到用户,将其状态改为true即可。
接下来需要根据激活码查询用户。对于没有找到用户和激活失败都要有对应的提示
点击激活,就可以出现这样。
对于激活码不对的用户,则会显示失败
前台用户登录_编写前台登录界面
实现:让他这里输入账号,手机,邮箱任何一个方式,都能登陆进去。
前台用户登录_编写登录代码
public Result login(String name,String password){
Member member = null;
// 查询用户名
if (member == null){
QueryWrapper<Member> queryWrapper = new QueryWrapper();
queryWrapper.eq("username",name);
member = memberMapper.selectOne(queryWrapper);
}
// 查询手机
if (member == null){
QueryWrapper<Member> queryWrapper = new QueryWrapper();
queryWrapper.eq("phoneNum",name);
member = memberMapper.selectOne(queryWrapper);
}
// 查询邮箱
if (member == null){
QueryWrapper<Member> queryWrapper = new QueryWrapper();
queryWrapper.eq("email",name);
member = memberMapper.selectOne(queryWrapper);
}
// 没有查询到用户
if(member == null){
return new Result(false,"用户名或密码错误");
}
// 验证密码
boolean flag = encoder.matches(password, member.getPassword());
if(!flag){
return new Result(false,"用户名或密码错误");
}
// 验证是否激活
if(!member.isActive()){
return new Result(false,"用户未激活,请登录邮箱激活用户!");
}
return new Result(true,"登录成功!",member);
}
这样通过多重的一个验证,来达到对不同方式的登陆,同时可以检查该用户是否完成了激活
修改header.html
,如果用户未登录,在最上方显示登录&注册
,如果用户已登录,在最上方显示用户名&我的收藏&退出
。
前台用户登录_编写登出方法
@RequestMapping("/logout")
public String logout(HttpSession session){
session.removeAttribute("member");
return "redirect:/frontdesk/index";
}
只需要把session的暑假删了就行了,肥肠简单
前台用户登录_编写登录拦截器
用户端的大部分网页都不需要用户登录访问,但收藏产品需要。接下来我们编写登录拦截器,验证收藏时用户是否登录。
此为---游客访问专属模式(游客模式)
首先编写网站用户拦截器MemberInterceptor
public class MemberInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从session中获取用户信息
Object member = request.getSession().getAttribute("member");
if (null == member){
response.sendRedirect(request.getContextPath()+"/frontdesk/login");
return false;
}
return true;
}
}
其次
编写拦截器配置类InterceptorConfig
//拦截器配置类
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//创建拦截器对象并指定其拦截的路径
registry.addInterceptor(new MemberInterceptor())
.addPathPatterns("/frontdesk/favorite/**");
}
}
由于
还没有编写收藏功能,先创建测试控制器代替,看看能不能用
@Controller
@RequestMapping("/frontdesk/favorite")
public class FavoriteController {
@RequestMapping("/test")
@ResponseBody
public String test() {
return "测试";
}
}
访问http://localhost/frontdesk/favorite/test
前台产品列表_优化header
实际开发中,产品类型列表要从后台动态查询。由于每个网页都要引入header,如果我们每次访问网页前都查询产品类型列表并放入model中非常的繁琐。所以推荐在header中使用ajax异步查询产品类型列表,这样只修改header.html
即可。
查询产品列表一共有两种方式,第一种方式就是根据产品类型来查询产品列表,就是这个类型下面有哪些产品。比如我点一个国内游,跳到这样一个页面,
(好像有点问题)
这个页面里面是所有的这样的一个类型下面的产品列表。还有一种方式就是上面这样的一个搜索框,我在里面写个上海,那么他搜索的时候就不是根据类型搜索了,而是根据关键词。那么放在数据库里面根据类型搜索,其实就是根据cid
那么根据关键词搜索就是根据productName,来进行模糊查询。当然这两种搜索反正最终都要跳到这样个列表页,那么我们首先先来说一下根据类型来查询。上面的东西是在header.html里面写死了的,
但是正常情况下他不应该也不能被写死,所以我们后台有什么产品,这里就需要去显示什么产品
前台产品列表_优化header
实际开发中,产品类型列表要从后台动态查询。由于在每个网页都要引入header,如果我们每次访问网页前都查询产品类型列表并放入model中非常的繁琐。所以推荐在header中使用ajax异步查询产品类型列表,这样只修改header.html
即可,大大减少了我的工作量,(摸鱼
创建FrontdeskCategoryController
,它是网站前台使用的产品类型控制器。
@Controller
@RequestMapping("/frontdesk/category")
public class FrontdeskCategoryController {
@Autowired
private CategoryService categoryService;
@RequestMapping("/all")
@ResponseBody
public List<Category> all() {
return categoryService.findAll();
}
}
注意
这里我们需要额外加一个注解:@ResponseBody
因为这个方法它不是我们普通的一个同步请求,要跳转回网页。而是一个异步请求
但是!异步请求网页是不刷新的,只往回返回数据,不进行页面的跳转
那么只往回返回数据,就给返回值写一个ResponseBody,就可以往回返回一段Json数据了
然后去修改header.html,让它异步的访问控制器
<script>
$(function (){
$.get("/frontdesk/category/all",function (categories) {
var str = "<li class=\"nav-active\"><a href=\"index.html\">首页</a></li>";
for (var i = 0 ; i <categories.length; i++){
str += "<li><a href=\"/frontdesk/product/routeList?cid="+categories[i].cid+"\">"+categories[i].cname+"</a></li>"
}
$(".nav").html(str);
})
})
</script>
前台产品列表_后端代码
旅游产品列表有两种查询方式:根据类型id查询,以及根据关键字查询。
先编写分页查询列表的后台代码:
ProductService
public Page<Product> findProduct(Integer cid,String productName,int page, int size){
QueryWrapper<Product> queryWrapper = new QueryWrapper<>();
if (cid != null){
queryWrapper.eq("cid",cid);
}
if (StringUtils.hasText(productName)){
queryWrapper.like("productName",productName);
}
// 还在启用的旅游产品
queryWrapper.eq("status",1);
// 倒序排列
queryWrapper.orderByDesc("pid");
Page selectPage = productMapper.selectPage(new Page(page,size),queryWrapper);
return selectPage;
}
随后在frontdesk里面搞一个FrontdeskProductController:
编写FrontdeskProductController
@RestController
@RequestMapping("/frontdesk/product")
public class FrontdeskProductController {
@Autowired
private ProductService productService;
/**
* 查询旅游线路
*
* @param cid 线路类别id
* @param productName 线路名
* @param page 页数
* @param size 每页条数
* @return
*/
@RequestMapping("/routeList")
public ModelAndView findProduct(Integer cid,
String productName,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
ModelAndView modelAndView = new ModelAndView();
Page<Product> productPage = productService.findProduct(cid, productName, page, size);
modelAndView.addObject("productPage", productPage);
modelAndView.addObject("cid", cid);
modelAndView.addObject("productName", productName);
modelAndView.setViewName("/frontdesk/route_list");
return modelAndView;
}
}
前台产品列表_前端页面
项目到这里已经接近尾声了,做一下用户看的产品列表,差不多要结束了
给前端的搜索框优化一下,让用户可以直接查询到指定位置的旅游
这里给搜索到的产品弄一个分页查询,复制之前管理员端的分页查询过来
每10页为一组的一个分页查询
前台产品详情_查询产品
接下来编写一下查询到产品,查看产品详情的功能。
在FrontdeskProductController中补一个查询线路的方法
// 线路详情
@RequestMapping("/routeDetail")
public ModelAndView findOne(Integer pid){
ModelAndView modelAndView = new ModelAndView();
Product product = productService.findOne(pid);
modelAndView.addObject("product",product);
modelAndView.setViewName("/frontdesk/route_detail");
return modelAndView;
}
查询结果:
下方须知
前台产品详情_收藏按钮
详情页中有收藏按钮。如果用户没有收藏该产品,显示立即收藏
按钮
如果用户已经收藏该产品,显示取消收藏
按钮,所以在查询产品详情时还要查询用户是否收藏该产品。
首先,在ProductMapper里面写一个方法
int findFavoriteByPidAndMid(@Param("pid") Integer pid,@Param("mid") Integer mid);
然后是ProductMapper.xml
<select id="findFavoriteByPidAndMid" resultType="int">
SELECT COUNT(*)
FROM favorite
where pid = #{pid}
AND mid = #{mid}
</select>
查看我的收藏:
查询收藏实际上是根据用户ID找到他所有的对应的产品ID
前台我的收藏_前端页面
这个页面可以照抄全部商品的页面,所以
结尾
项目到这里已经做完了,剩下的工作就是将他部署在服务器上
项目部署_安装Docker环境
为了节约资源,在生产环境中我们更多的是使用Docker容器部署SpringBoot应用
首先我们准备Docker环境
1,准备一台centos7系统的虚拟机,连接虚拟机。
2,关闭虚拟机防火墙
# 关闭运行的防火墙
systemctl stop firewalld.service
# 禁止防火墙自启动
systemctl disable firewalld.service
3,安装Docker开启远程docker服务
# 安装Docker
yum -y install docker
# 启动docker
systemctl start docker
4,开启远程docker服务
# 修改docker配置文件
vim /lib/systemd/system/docker.service
# 在ExecStart=后添加配置,远程访问docker的端口为2375
ExecStart=/usr/bin/dockerd-current -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock \
--add-runtime docker-runc=/usr/libexec/docker/docker-runc-current \
--default-runtime=docker-runc \
--exec-opt native.cgroupdriver=systemd \
--userland-proxy-path=/usr/libexec/docker/docker-proxy-current \
--init-path=/usr/libexec/docker/docker-init-current \
--seccomp-profile=/etc/docker/seccomp.json \
$OPTIONS \
$DOCKER_STORAGE_OPTIONS \
$DOCKER_NETWORK_OPTIONS \
$ADD_REGISTRY \
$BLOCK_REGISTRY \
$INSECURE_REGISTRY \
$REGISTRIES
# 重启docker
systemctl daemon-reload
systemctl restart docker
项目部署_安装Mysql容器
-
拉取mysql镜像
docker pull mysql:5.7
-
启动容器
docker run -itd --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root mysql:5.7
-
使用Navicat连接mysql
-
将开发环境数据库导入生产环境数据库
项目部署_修改配置文件
将yml文件适配生产环境
# 端口
server:
port: 80
# 数据源
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql:///travel?serverTimezone=UTC
url: jdbc:mysql://192.168.0.182:3306/travel?serverTimezone=UTC
username: root
password: root
# 上传文件
servlet:
multipart:
max-file-size: 10MB # 最大单个文件
max-request-size: 10MB # 一次请求最大上传
# 打成jar包必须添加如下配置才能找到页面
thymeleaf:
mode: HTML
cache: false
prefix: classpath:/templates
#配置mybatis-plus
mybatis-plus:
global-config:
db-config:
# 主键生成策略为自增
id-type: auto
configuration:
# 关闭列名自动驼峰命名规则映射
map-underscore-to-camel-case: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启sql日志
# 日志格式
logging:
pattern:
console: '%d{HH:mm:ss.SSS} %clr(%-5level) --- [%-15thread] %cyan(%-50logger{50}):%msg%n'
# 发送邮件配置
mail:
# 发件人地址
user: 461618768@qq.com
# 发件人密码
password: xtyzfgwefpcebged
# 项目路径
project:
# path: http://localhost
path: http://192.168.0.182
项目部署_Maven插件制作镜像
1,在项目的pom文件中添加docker-maven-plugin
插件
<!-- docker-maven-plugin-->
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>1.2.2</version>
<configuration>
<!-- Docker路径 -->
<dockerHost>http://192.168.0.182:2375</dockerHost>
<!-- Dockerfile定义 -->
<baseImage>openjdk:11</baseImage>
<!-- 作者 -->
<maintainer>itbaizhan</maintainer>
<resources>
<resource>
<!-- 复制jar包到docker容器指定目录 -->
<targetPath>/</targetPath>
<!-- 从哪个包拷贝文件,target包 -->
<directory>${project.build.directory}</directory>
<!-- 拷贝哪个文件 -->
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
<workdir>/</workdir>
<entryPoint>["java", "-jar", "${project.build.finalName}.jar"]</entryPoint>
<forceTags>true</forceTags>
<!-- 镜像名 -->
<imageName>${project.artifactId}</imageName>
<!-- 镜像版本 -->
<imageTags>
<imageTag>${project.version}</imageTag>
</imageTags>
</configuration>
</plugin>
2,使用maven的package命令给项目打包
3,使用maven的docker插件制作镜像
4,查看所有的镜像
docker images
5,启动容器
docker run -d -p 80:80 travel:0.0.1-SNAPSHOT
6,拿另一台电脑访问网址查看是否启动成功