暗星·Java实战之进阶旅游网

news2024/9/24 23:31:29

简介

暗星旅游网,是一个分为管理员端和用户端的项目,有权限分离认证,管理员端(后台)进行旅游产品的维护,主要功能有:管理员管理,角色管理,权限管理,认证和授权,产品类型管理,旅游产品管理;用户端(前台)进行旅游产品的展示,主要功能有:用户注册和登录,查询旅游产品,收藏旅游产品。通过开发本项目后,我可以分析并开发常见的门户网站。

项目地址:

注意事项!!!!!

项目涉及路径,必须直接放在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

  1. 在前端页面中引入wangEditor.js

  2. 在页面中加入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>

修改一下注册页面的代码

前台用户注册_编写注册方法

为了保证用户注册的信息是真实的,往往在用户注册后不能直接登录,而需要用户激活后才能登录。用户注册激活的步骤如下:

  1. 用户在页面填写个人信息,发送到后端代码。

  2. 后端验证数据后保存用户信息,但此时用户的状态为false,还不能登录。

  3. 后端拿到用户输入的邮箱,给用户邮箱发送一段随机字符串,并将该字符串保存到数据库的用户表中。

  4. 用户登录个人邮箱,点击随机字符串访问项目,项目将该拥有字符串的用户状态变为true,此时用户即可登录。

大致如此,以此防止有人大量创建用户,来攻击数据库,造成大量数据给服务器带来卡顿

  1. 用户在页面填写个人信息,发送到后端代码。

  2. 后端验证数据后保存用户信息,但此时用户的状态为false,还不能登录。

  3. 后端拿到用户输入的邮箱,给用户邮箱发送一段随机字符串,并将该字符串保存到数据库的用户表中。

  4. 用户登录个人邮箱,点击随机字符串访问项目,项目将该拥有字符串的用户状态变为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);
    }
}

提示:打断点,进来追踪一下数据

效果展示:

稍后把这个注册成功的页面修复一下下

前台用户注册_发送邮件配置

在用户注册成功后,要向用户的邮箱发送一封激活邮件,发送邮件需要在系统中配置发件人,同学们使用自己的邮箱作为发件人即可。

  1. 配置邮箱第三方登录。

    我们在系统中使用邮箱发送邮件属于第三方登录,而市面上的邮箱默认是不能第三方登录的。我们需要登录邮箱,配置第三方登录。

这里牺牲一下下某人的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容器

  1. 拉取mysql镜像

    docker pull mysql:5.7
  2. 启动容器

    docker run -itd --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root mysql:5.7

  3. 使用Navicat连接mysql

  4. 将开发环境数据库导入生产环境数据库

项目部署_修改配置文件

将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,拿另一台电脑访问网址查看是否启动成功

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2121285.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python怎么发送邮件:基础步骤与详细教程?

Python怎么发送邮件带附件&#xff1f;怎么使用Python发送邮件&#xff1f; 无论是工作中的通知、报告&#xff0c;还是生活中的问候、邀请&#xff0c;电子邮件都扮演着不可或缺的角色。那么&#xff0c;Python怎么发送邮件呢&#xff1f;AokSend将详细介绍Python发送邮件的基…

三、排序算法

文章目录 一、排序算法的介绍二、算法的时间复杂度2.1 基本介绍2.2 时间频度2.3 时间复杂度的表示2.4 算法的平均时间复杂度和最坏时间复杂度 三、算法的空间复杂度四、排序算法4.1 交换排序4.1.1 冒泡排序4.1.2 快速排序 4.2 选择排序4.2.1 简单选择排序4.2.2 堆排序 4.3 插入…

基于Llama 3的最强开源医疗AI模型发布,刷新榜单

转自&#xff1a;火星AIGC 一家名为Saama AI Labs发布了他们基于Llama 3 微调的开源医疗AI大模型OpenBioLLM-Llama3-70B 和 OpenBioLLM-Llama3-8B&#xff0c;刷新抱抱脸上的医疗大模型榜单&#xff0c;并占据榜首。其在生物医学领域的测试性能超越 GPT-4、Gemini、Meditron-7…

浙大数据结构:04-树6 Complete Binary Search Tree

这道题利用了完全二叉树的性质&#xff0c;我也参考了一些代码写的。 &#xff08;自己一开始写了别的方法&#xff0c;但一直过不了最后一个测试点&#xff0c;红温了&#xff09; 机翻&#xff1a; 1、条件准备 用vector存输入的数据&#xff0c;另一个数组存输出的结果&a…

文心一言 VS 讯飞星火 VS chatgpt (344)-- 算法导论23.2 3题

三、对于稀疏图 G ( V &#xff0c; E ) G(V&#xff0c;E) G(V&#xff0c;E)&#xff0c;这里 ∣ E ∣ Θ ( V ) |E|Θ(V) ∣E∣Θ(V)&#xff0c;使用斐波那契堆实现的Prim算法是否比使用二叉堆实现的算法更快&#xff1f;对于稠密图又如何呢&#xff1f; ∣ E ∣ |E| ∣E…

FPGA技术赋能云数据中心:提高性能与效率

随着现代科技的迅猛发展和大数据时代的推动&#xff0c;云数据中心已成为众多企业的核心基础设施。然而&#xff0c;伴随数据处理需求的不断增长&#xff0c;传统硬件架构在性能、功耗和灵活性方面面临诸多挑战。为了解决这些问题&#xff0c;FPGA&#xff08;现场可编程门阵列…

辽宁汇聚公益慈善力量,绿葆网络助力辽宁绿色生态建设,彰显企业大爱

9月5日&#xff0c;于辽宁省沈阳市隆重举行的“中华慈善日”主题宣传活动暨“山海有情 天辽地宁”即开型福利彩票发行、“生态公益林”项目启动仪式上&#xff0c;广州绿葆网络发展有限公司作为受邀企业之一&#xff0c;积极履行社会责任&#xff0c;向辽宁省慈善联合总会捐赠了…

坐牢第三十六天(QT)

自定义QQ界面 wedget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QDebug> //qt中信息调试类 #include <QIcon> //图标类 #include <QPushButton>//按钮类 #include <QLabel> //标签类 #include <QMovie> //动图类…

【C++】—— vector 的模拟实现

【C】—— vector 的模拟实现 0 前言1 vector 的成员变量1.1 stl 库中的 vector 成员变量1.2 模拟实现 vector 成员变量 2 迭代器3 size、capacity、empty4 opreator[ ]5 reserve5.1 初版 reserve5.2 _finish 的处理5.3 深拷贝5.4 终版 6 push_back 与 pop_back7 打印函数7.1 初…

打破界限,自闭症寄宿学校带给孩子的改变

在社会的广阔画卷中&#xff0c;有一群特别的孩子&#xff0c;他们以独特的视角感知世界&#xff0c;以非凡的方式表达情感&#xff0c;他们就是自闭症儿童。自闭症&#xff0c;这个听起来略带神秘色彩的词汇&#xff0c;实则承载着无数家庭的期盼与挑战。在这片充满爱的土地上…

【北京迅为】《STM32MP157开发板使用手册》-第十六章 Buildroot制作根文件系统

iTOP-STM32MP157开发板采用ST推出的双核cortex-A7单核cortex-M4异构处理器&#xff0c;既可用Linux、又可以用于STM32单片机开发。开发板采用核心板底板结构&#xff0c;主频650M、1G内存、8G存储&#xff0c;核心板采用工业级板对板连接器&#xff0c;高可靠&#xff0c;牢固耐…

丰巢“闯关”港交所上市

社区中随处可见的智能快递柜&#xff0c;即将捧出一个IPO。 近日&#xff0c;丰巢控股有限公司&#xff08;下称“丰巢控股”或“丰巢”&#xff09;正式向港交所递交了招股书&#xff0c;华泰国际担任其独家保荐人。这将是继顺丰控股、顺丰房托、嘉里物流、顺丰同城之后&…

【Qt笔记】QGroupBox控件详解

目录 引言 一、基本属性 二、常用方法 2.1 构造函数 2.2 设置标题 2.3 设置复选框模式 2.4 是否被选中 2.5 设置对齐方式 2.6 设置扁平化样式 三、信号与槽机制 四、样式定制 五、应用示例 5.1 代码 5.2 代码解析 5.3 实现效果 结语 引言 QGroupBox 是 Qt…

检查iOS多语系文件内容检查iOS多语系文件内容

在iOS中&#xff0c;检查多语言文件&#xff08;如 .strings 文件&#xff09;内容的命令通常使用 plutil 工具。你可以通过终端执行以下命令来检查 .strings 文件的格式和内容&#xff1a; plutil -lint path/to/your/Localizable.strings 这个命令会验证指定的 .strings 文…

C语言13--结构体

结构体基本概念 C语言提供了众多的基本类型&#xff0c;但现实生活中的对象一般都不是单纯的整型、浮点型或字符串&#xff0c;而是这些基本类型的综合体。比如一个学生&#xff0c;典型地应该拥有学号&#xff08;整型&#xff09;、姓名&#xff08;字符串&#xff09;、分数…

已配置好的Linux CentOS7虚拟机转换为可视化界面问题

一、发现问题 学习过程中发现可视化界面比较有意思&#xff0c;就想尝试搞一下看看&#xff0c;于是去网站上搜索&#xff0c;看到的一些是在新建虚拟机的时候进行设置的&#xff0c;我尝试跟着步骤去搞&#xff0c;发现其中最关键的一步&#xff0c;软件选择中&#xff0c;没有…

【北京迅为】《STM32MP157开发板使用手册》- 第十五章 制作最小linux系统

iTOP-STM32MP157开发板采用ST推出的双核cortex-A7单核cortex-M4异构处理器&#xff0c;既可用Linux、又可以用于STM32单片机开发。开发板采用核心板底板结构&#xff0c;主频650M、1G内存、8G存储&#xff0c;核心板采用工业级板对板连接器&#xff0c;高可靠&#xff0c;牢固耐…

TikTok内容电商:短视频与直播带货如何重塑消费者购物决策

数字化时代&#xff0c;内容电商已经成为一种重要的商业模式。而TikTok作为全球领先的短视频平台&#xff0c;其内容电商模式正慢慢改变用户的消费习惯。TikTok Shop作为TikTok平台上的电商板块&#xff0c;也凭借其独特的短视频和直播带货模式&#xff0c;影响着消费者的购物决…

百度MEG数据开发治理平台-TDS

导读 百度MEG的上一代大数据产品存在平台分散、质量不均和易用性差等问题&#xff0c;导致开发效率低下、学习成本高&#xff0c;业务需求响应迟缓。为了解决这些问题&#xff0c;百度MEG内部开发了图灵3.0生态系统。图灵3.0覆盖了数据全生命周期&#xff0c;包括Turing Data …

AI在医学领域:HMARL首个多器官诊断AI框架

多器官疾病因其对多个器官系统的同时影响而带来了显著的挑战&#xff0c;这需要复杂和适应性的治疗策略。尽管在人工智能驱动的医疗决策支持系统方面取得了最新进展&#xff0c;但现有的解决方案通常限于单个器官系统。它们往往忽视了器官系统之间复杂的相互依赖性&#xff0c;…