瑞吉外卖-Day02


title: 瑞吉外卖-Day02
abbrlink: ‘1’
date: 2023-04-1 19:30:00

瑞吉外卖-Day02

课程内容

  • 完善登录功能
  • 新增员工
  • 员工信息分页查询
  • 启用/禁用员工账号
  • 编辑员工信息

分析前端页面效果是如何实现的

为什么点击左边 右边会根着变化

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WwJYwseg-1688460908030)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202304092100003.png)]

首先 我们先来看一下菜单是如何展示出来的 在来看一下 为啥点击菜单时 右边会跟着变

第一 :菜单是如何展示出来的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a6J6csVc-1688460908031)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202304092101695.png)]

我们在创建一个VUE对象的时候 在我们的 data() 的位置 我们准备了 menuList: [] 的一个数据 menuList: [] 是一个数组对象

数组对象里面放着一个个JSON数据 而每一个JSON数据里面又放着id name url icon(图标) 以上只是数据的准备 并不是定义就可以展示到菜单的一个位置

<script>

      new Vue({
        el: '#app',
        data() {
          return {
            defAct: '2',
            menuActived: '2',
            userInfo: {},
            menuList: [
              // {
              //   id: '1',
              //   name: '门店管理',
              //   children: [
                  {
                    id: '2',
                    name: '员工管理',
                    url: 'page/member/list.html',
                    icon: 'icon-member'
                  },
                  {
                    id: '3',
                    name: '分类管理',
                    url: 'page/category/list.html',
                    icon: 'icon-category'
                  },
                  {
                    id: '4',
                    name: '菜品管理',
                    url: 'page/food/list.html',
                    icon: 'icon-food'
                  },
                  {
                    id: '5',
                    name: '套餐管理',
                    url: 'page/combo/list.html',
                    icon: 'icon-combo'
                  },
                  {
                    id: '6',
                    name: '订单明细',
                    url: 'page/order/list.html',
                    icon: 'icon-order'
                  }
              //   ],
              // },
            ],
            iframeUrl: 'page/member/list.html',
            headTitle: '员工管理',
            goBackFlag: false,
            loading: true,
            timer: null
          }
        },
        computed: {},
        created() {
          const userInfo = window.localStorage.getItem('userInfo')
          if (userInfo) {
            this.userInfo = JSON.parse(userInfo)
          }
          this.closeLoading()
        },
        beforeDestroy() {
          this.timer = null
          clearTimeout(this.timer)
        },
        mounted() {
          window.menuHandle = this.menuHandle
        },
        methods: {
          logout() {
            logoutApi().then((res)=>{00
              if(res.code === 1){
                localStorage.removeItem('userInfo')
                window.location.href = '/backend/page/login/login.html'
              }
            })
          },
          goBack() {
            // window.location.href = 'javascript:history.go(-1)'
            const menu = this.menuList.find(item=>item.id===this.menuActived)
            // this.goBackFlag = false
            // this.headTitle = menu.name
            this.menuHandle(menu,false)
          },
          menuHandle(item, goBackFlag) {
            this.loading = true
            this.menuActived = item.id
            this.iframeUrl = item.url
            this.headTitle = item.name
            this.goBackFlag = goBackFlag
            this.closeLoading()
          },
          closeLoading(){
            this.timer = null
            this.timer = setTimeout(()=>{
              this.loading = false
            },1000)
          }
        }
      })
    </script>

图3

 menuList: [    menuList: [] 是一个数组对象 数组对象       //120行
                    
                  {                      里面放着一个个JSON数据
                    id: '2',           每一个JSON数据里面又放着id name url icon(图标) 
                    name: '员工管理',
                    url: 'page/member/list.html',
                    icon: 'icon-member'
                  },
                  {
                    id: '3',
                    name: '分类管理',
                    url: 'page/category/list.html',
                    icon: 'icon-category'
                  },
                  {
                    id: '4',
                    name: '菜品管理',
                    url: 'page/food/list.html',
                    icon: 'icon-food'
                  },
                  {
                    id: '5',
                    name: '套餐管理',
                    url: 'page/combo/list.html',
                    icon: 'icon-combo'
                  },
                  {
                    id: '6',
                    name: '订单明细',
                    url: 'page/order/list.html',
                    icon: 'icon-order'
                  }
              //   ],
              // },
            ],

name 有的是’员工管理’, ‘分类管理’, ‘菜品管理’, ‘套餐管理’, ‘订单明细’, 对应的就是我们下图所示、

图4

以上只是数据的准备 并不是定义就可以展示到菜单的一个位置

第二 怎么展示到菜单的位置

请到第39行 用到了el-menu组件 下面的v-for表示在遍历 遍历menuList

<el-submenu :index="item.id" v-if="item.children && item.children.length>0">

这里面的 v-if是不会成立的 但是会执行第64行<el-menu-item v-else :index=“item.id” @click=“menuHandle(item,false)”>这个分支 然后就可以把{{item.name}} item.name给展示出来

 <el-menu             //菜单组件
              :default-active="defAct"
              :unique-opened="false"
              :collapse-transition="false"
              background-color="#343744"
              text-color="#bfcbd9"
              active-text-color="#f4f4f5"
            >     //item不是固定写法
              <div v-for="item in menuList" :key="item.id">    //v-for表示在遍历 遍历menuList
                  //判断item里面有没有children属性和children属性的length是否大于0
                  //我们往下看到第126那里的每一项都没有我们的children
               // 但是会执行第64行<el-menu-item v-else :index="item.id" @click="menuHandle(item,false)">这个分支                  // 然后就可以把第66行的item.name给展示出来 如果我们把id修改成Id
                <el-submenu :index="item.id" v-if="item.children && item.children.length>0">
                  <template slot="title">
                    <i class="iconfont" :class="item.icon"></i>
                    <span>{{item.name}}</span>
                  </template>
                  <el-menu-item
                    v-for="sub in item.children"
                    :index="sub.id"
                    :key="sub.id"
                    @click="menuHandle(sub,false)"
                    >
                    <i :class="iconfont" :class="sub.icon"></i>
                    <span slot="title">{{sub.name}}</span>
                    </el-menu-item
                  >
                </el-submenu>
      //64行          <el-menu-item v-else :index="item.id" @click="menuHandle(item,false)">
                  <i class="iconfont" :class="item.icon"></i>
   //66行               <span slot="title">{{item.name}}</span>     //把item.name给展示出来
                </el-menu-item>
              </div>
            </el-menu>

图5

所以我们看到的就是以下的名称

图6

如果我们把name修改成id

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VueK0LVa-1688460908036)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202304092342644.png)]

重启 刷新

图8

为什么是Id 因为由这个对象数组menuList:[]决定

图9

`new Vue({
        el: '#app',
        data() {
          return {
            defAct: '2',
            menuActived: '2',
            userInfo: {},
            menuList: [
              // {
              //   id: '1',
              //   name: '门店管理',
              //   children: [
                  {
                    id: '2',
                    name: '员工管理',
                    url: 'page/member/list.html',
                    icon: 'icon-member'
                  },
                  {
                    id: '3',
                    name: '分类管理',
                    url: 'page/category/list.html',
                    icon: 'icon-category'
                  },
                  {
                    id: '4',
                    name: '菜品管理',
                    url: 'page/food/list.html',
                    icon: 'icon-food'
                  },
                  {
                    id: '5',
                    name: '套餐管理',
                    url: 'page/combo/list.html',
                    icon: 'icon-combo'
                  },
                  {
                    id: '6',
                    name: '订单明细',
                    url: 'page/order/list.html',
                    icon: 'icon-order'
                  }
              //   ],
              // },
            ],

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L8eI3NKi-1688460908040)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202304092344576.png)]

图11

第三 : 点击菜单时 右边会跟着变

http://localhost:8080/backend/index.html

这些菜单实际上都加了一个@click事件 当我们点击菜单的事情 他会执行menuHandle() 方法

  <el-menu-item v-else :index="item.id" @click="menuHandle(item,false)">

图12

然后把item,false传过来 接下来我们找到menuHandle() 方法 在196行

menuHandle(item, goBackFlag) {
            this.loading = true
            this.menuActived = item.id
            this.iframeUrl = item.url
            this.headTitle = item.name
            this.goBackFlag = goBackFlag
            this.closeLoading()
          },

当我们点击菜单的时候 就会执行menuHandle() 方法 这里面最重要的是 this.iframeUrl = item.url

这里面的url就是以下的url(128行) 也就是点击菜单的url

 menuList: [
              // {
              //   id: '1',
              //   name: '门店管理',
              //   children: [
                  {
                    id:'2',
                    name : '员工管理',
                    url: 'page/member/list.html',
                    icon: 'icon-member'
                  },
                  {
                    id: '3',
                    name: '分类管理',
                    url: 'page/category/list.html',
                    icon: 'icon-category'
                  },
                  {
                    id: '4',
                    name: '菜品管理',
                    url: 'page/food/list.html',
                    icon: 'icon-food'
                  },
                  {
                    id: '5',
                    name: '套餐管理',
                    url: 'page/combo/list.html',
                    icon: 'icon-combo'
                  },
                  {
                    id: '6',
                    name: '订单明细',
                    url: 'page/order/list.html',
                    icon: 'icon-order'
                  }
              //   ],
              // },
            ],

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FGiI0VQ9-1688460908045)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202304100024943.png)]

这里面是通过iframeUrl的方式来显示新的页面

iframeUrl在第158行定义的

iframeUrl: 'page/member/list.html',

image-20230410002610988

在什么地方会用到iframeUrl呢?

在第89行用到

<iframe></iframe>

这个是用来展示一个新的页面的 而这个页面从哪里来呢

            <iframe         //用来展示一个新的页面
              id="cIframe"
              class="c_iframe"
              name="cIframe"
              :src="iframeUrl"
              width="100%"
              height="auto"
              frameborder="0"
              v-show="!loading"
            ></iframe>

image-20230410002952927

这个相当于说在我们的页面上挖了一个坑

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HHPZH34n-1688460908048)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202304100031207.png)]

而这个页面从哪里来呢

 <iframe
              id="cIframe"
              class="c_iframe"
              name="cIframe"
              :src="iframeUrl"   这个地方传进什么数据  展示的就是什么数据
              width="100%"
              height="auto"
              frameborder="0"
              v-show="!loading"
            ></iframe>

image-20230410003635226

为什么我们登录之后 展示的是员工管理 而不是分类管理或是其他

这是因为我们给iframeUrl一个初始值

 iframeUrl: 'page/member/list.html',

image-20230410003846840

而我们在点击菜单的时候 其实是在切换url

      menuHandle(item, goBackFlag) {
            this.loading = true
            this.menuActived = item.id
            this.iframeUrl = item.url
            this.headTitle = item.name
            this.goBackFlag = goBackFlag
            this.closeLoading()
          },

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lsGTKVNm-1688460908051)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131532890.png)]

比如说我们现在初始页面给修改成以下

iframeUrl: 'http://www.itcast.c

image-20230410004214015

重启 刷新

image-20230410004253475

所以当我们点击菜单的时候 其实他是在切换url地址

而这个url地址在数据准备的时候就已经写好了

 menuList: [
              // {
              //   id: '1',
              //   name: '门店管理',
              //   children: [
                  {
                    id:'2',
                    name : '员工管理',
                    url: 'page/member/list.html',
                    icon: 'icon-member'
                  },
                  {
                    id: '3',
                    name: '分类管理',
                    url: 'page/category/list.html',
                    icon: 'icon-category'
                  },
                  {
                    id: '4',
                    name: '菜品管理',
                    url: 'page/food/list.html',
                    icon: 'icon-food'
                  },
                  {
                    id: '5',
                    name: '套餐管理',
                    url: 'page/combo/list.html',
                    icon: 'icon-combo'
                  },
                  {
                    id: '6',
                    name: '订单明细',
                    url: 'page/order/list.html',
                    icon: 'icon-order'
                  }
              //   ],
              // },
            ],

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k2oreBW7-1688460908054)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202304100044727.png)]

image-20230410004631599

不同菜单对应不同的页面 所以当我们点击某一个菜单的时候 他其实是切换到了相应的页面

1. 完善登录功能

1.1 问题分析

前面我们已经完成了后台系统的员工登录功能开发,但是目前还存在一个问题,接下来我们来说明一个这个问题, 以及如何处理。

1). 目前现状

用户如果不登录,直接访问系统首页面,照样可以正常访问。

image-20210727232226862

2). 理想效果

上述这种设计并不合理,我们希望看到的效果应该 是,只有登录成功后才可以访问系统中的页面,如果没有登录, 访问系统中的任何界面都直接跳转到登录页面。

image-20210727232747276

那么,具体应该怎么实现呢?

可以使用我们之前讲解过的 过滤器、拦截器来实现,在过滤器、拦截器中拦截前端发起的请求,判断用户是否已经完成登录,如果没有登录则返回提示信息,跳转到登录页面。

1.2 思路分析

image-20210727233554707

过滤器具体的处理逻辑如下:

A. 获取本次请求的URI

B. 判断本次请求, 是否需要登录, 才可以访问

C. 如果不需要,则直接放行

D. 判断登录状态,如果已登录,则直接放行

E. 如果未登录, 则返回未登录结果

如果未登录,我们需要给前端返回什么样的结果呢? 这个时候, 我们可以去看看前端是如何处理的 ?

image-20210728001324901

1.3 代码实现

实现步骤:
1、创建自定义过滤器LoginCheckFilter
2、在启动类上加入注解@ServletComponentScan
3、完善过滤器的处理逻辑

1). 定义登录校验过滤器

自定义一个过滤器 LoginCheckFilter 并实现 Filter 接口, 在doFilter方法中完成校验的逻辑。 那么接下来, 我们就根据上述分析的步骤, 来完成具体的功能代码实现:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tEBf2g8a-1688460908057)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131533933.png)]

image-20230410010944730

package com.itheima.reggie.filter;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/*检查用户是否已经完成登录*/
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {


    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        //强转
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        log.info("拦截到请求:{}",request.getRequestURI());
          //放行
        filterChain.doFilter(request,response);
    }
}

image-20230410012037639

重启

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JcYZhW6y-1688460908060)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131533957.png)]

刷新

image-20230410012317239

image-20230410012420126

代码解释:

解释一

这段代码是一个 Java Web 应用程序中的过滤器(Filter)。 过滤器在请求到达目标 Servlet/JSP 前预处理请求或响应,以便在处理真正的请求之前完成一些操作。

这个过滤器的作用是检查用户是否已经登录,通过检查URL是否以登录用户的认证信息,如session ID、token等。如果用户没有登录,过滤器会将用户重定向到登录页面,如果用户已经登录,则请求将被放行到下一个处理程序。

具体来说,这个过滤器实现了Filter过滤器接口中的doFilter方法。该方法接收ServletRequest和ServletResponse类型的参数,并通过类型转换将其转换为HttpServletRequest和HttpServletResponse类型。然后,它会打印一条日志以表明请求已经被拦截,并将请求和响应对象传递给过滤器链(FilterChain)。最后,过滤器链将请求和响应对象传递给下一个处理程序。

总之,这个过滤器是一个很有用的工具,它能够在用户请求到达目标 Servlet/JSP 之前进行预处理,确保用户已经完成登录并且已经得到授权,从而保护Web应用程序的安全性。

解释二

当我们使用一个 Java Web 应用程序时,需要确保用户已经完成登录,并且已经得到授权,否则他们将无法访问需要特殊权限的页面。这就是这段代码的作用。

这段代码实现了一个过滤器(Filter),它会在请求到达目标 Servlet/JSP 之前进行预处理。它会检查用户是否已经完成登录,如果没有完成登录,它会将用户重定向到登录页面,以便用户能够完成登录,然后再次尝试访问需要特殊权限的页面。

这个过滤器检查请求的 URL,如果 URL 包含了用户的认证信息,比如 session ID 或者 token,那么它就认为用户已经完成了登录。如果用户没有完成登录,它就会重定向到登录页面,让用户完成登录。

这个过滤器的实现比较简单,它实现了 Filter 接口中的 doFilter 方法。当有请求到达时,它会将请求转换为 HttpServletRequest 对象,并检查是否已经完成登录。如果已经完成登录,它就会将请求和响应对象传递给下一个处理程序。如果没有完成登录,它就会将用户重定向到登录页面。

总之,这个过滤器是一个很有用的工具,它能够确保用户已经完成登录,并且已经得到授权,从而保护 Web 应用程序的安全性。

解释三

这段代码是一个 Java Web 应用程序中的过滤器(Filter),它的作用是在用户访问任何网页之前检查用户是否已经完成登录操作。

在代码中,使用了 @WebFilter 注解,指定了过滤器的名称为 “loginCheckFilter”,并将其应用于所有的 URL,即 “/*”。

在 doFilter() 方法中,将传入的 ServletRequest 和 ServletResponse 对象转换为 HttpServletRequest 和 HttpServletResponse 对象,并记录下当前请求的 URL 地址,最后将请求和响应对象交给 FilterChain 继续处理。

Filter 的作用类似于一个拦截器,当用户访问任何网页时,该过滤器会首先拦截请求,检查用户是否已经登录,如果用户已经完成了登录操作,那么该过滤器会放行请求,让用户继续访问该网页;如果用户还没有登录,那么该过滤器会重定向到登录页面,要求用户先完成登录操作,然后才能继续访问网页。

总的来说,这段代码的作用是保护 Web 应用程序中的敏感资源,确保只有已经完成登录操作的用户才能访问这些资源,提高了系统的安全性。

理解 :在这个代码中为什么要进行强转

在这个代码中,我们将 ServletRequest 和 ServletResponse 对象转换为了 HttpServletRequest 和 HttpServletResponse 对象,这是因为 Filter 接口的 doFilter 方法接收的是 ServletRequest 和 ServletResponse 对象,而我们需要使用 HttpServletRequest 和 HttpServletResponse 对象来获取 HTTP 请求和响应的相关信息。

ServletRequest 和 ServletResponse 是 Java Servlet 规范中定义的接口,它们提供了一种通用的处理 HTTP 请求和响应的方式。但是它们缺少 HttpServletRequest 和 HttpServletResponse 对象的一些方法,例如 getParameter 和 getAttribute 等方法,这些方法是在 HttpServletRequest 和 HttpServletResponse 接口中定义的。

因此,为了能够使用这些特定的方法,我们需要将 ServletRequest 和 ServletResponse 对象强制转换为 HttpServletRequest 和 HttpServletResponse 对象,这样就可以使用这些特定的方法了。但是,在进行强制转换之前,我们需要确保请求确实是 HTTP 请求,否则强制转换可能会导致类型转换异常。

两个注解:@WebFilter 和 @Slf4j的解释

@WebFilter 注解用于标记一个类为过滤器(Filter),它可以指定过滤器的名称(filterName)和 URL 模式(urlPatterns)。在这个例子中,我们将这个过滤器的名称设置为 “loginCheckFilter”,并指定了 URL 模式为 “/*”,也就是拦截所有的请求。

@Slf4j 注解是 Lombok 框架中提供的注解之一,它用于简化日志记录的代码。它会在编译时自动生成一个名为 “log” 的日志记录器,我们可以使用它来记录日志。在这个例子中,我们使用 log.info 方法来记录日志信息。

这些注解都是用来帮助我们更方便地编写代码和实现功能的。@WebFilter 注解让我们更容易地定义一个过滤器,并指定需要拦截的请求。@Slf4j 注解则帮助我们更方便地实现日志记录功能。

每行代码的详细解释

@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")

这行代码是使用 @WebFilter 注解声明一个过滤器,并指定了过滤器的名称为 “loginCheckFilter”,并将其应用于所有的 URL,即 “/*”。

@Slf4j

这行代码使用了 Lombok 提供的 @Slf4j 注解,自动生成了一个名为 log 的日志对象,可以用它来输出日志信息。

public class LoginCheckFilter implements Filter {}

这行代码是 doFilter() 方法的声明,该方法是 Filter 接口中的一个方法,用于拦截用户请求,并进行处理。

HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;

这行代码将传入的 ServletRequest 和 ServletResponse 对象转换为 HttpServletRequest 和 HttpServletResponse 对象,以便后续对请求和响应进行处理。

log.info("拦截到请求:{}", request.getRequestURI());

这行代码使用之前定义的 log 对象输出一条日志信息,记录下当前请求的 URL 地址,方便后续的调试和问题定位。

filterChain.doFilter(request, response);

这行代码调用 FilterChain 对象的 doFilter() 方法,将处理过的请求和响应对象传递给下一个过滤器或 Servlet 进行处理,如果没有其他过滤器,则直接交给 Servlet 处理。这样,过滤器的链条就得以形成,确保每个请求都会被多个过滤器进行处理。

复习过滤器filter

解释一

HTTP Request --> Filter 1 --> Filter 2 --> ... --> Filter n --> Servlet --> HTTP Response

在Java Web应用程序中,过滤器(Filter)是一个用于处理HTTP请求和响应的组件。它们被用来拦截请求并根据需要修改请求参数、头信息或请求体,以及响应信息。过滤器可以在 Servlet 执行之前或之后进行处理,也可以在 Servlet 进行处理时同时进行处理。

当一个HTTP请求到达Servlet容器时,它会首先被任何注册的过滤器拦截。过滤器根据需要可以更改请求,或者将请求传递给下一个过滤器。一旦所有过滤器都已处理请求,最终的目标Servlet会被调用。Servlet处理请求后生成响应,该响应会通过相同的过滤器链进行处理,并且在每个过滤器处可以修改响应。

这个过滤器链的顺序是在web.xml文件中定义的,并且过滤器可以动态添加或删除。这使得开发人员可以根据需要自定义请求处理流程,从而实现更加灵活和复杂的应用程序功能。

所属包: com.itheima.reggie.filter

import com.alibaba.fastjson.JSON;
import com.itheima.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 检查用户是否已经完成登录
 */
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter{
    //路径匹配器,支持通配符
    public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        //1、获取本次请求的URI
        String requestURI = request.getRequestURI();// /backend/index.html

        log.info("拦截到请求:{}",requestURI);

        //定义不需要处理的请求路径
        String[] urls = new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**"
        };
		
        //2、判断本次请求是否需要处理
        boolean check = check(urls, requestURI);

        //3、如果不需要处理,则直接放行
        if(check){
            log.info("本次请求{}不需要处理",requestURI);
            filterChain.doFilter(request,response);
            return;
        }
		
        //4、判断登录状态,如果已登录,则直接放行
        if(request.getSession().getAttribute("employee") != null){
            log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
            filterChain.doFilter(request,response);
            return;
        }

        log.info("用户未登录");
        //5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;

    }

    /**
     * 路径匹配,检查本次请求是否需要放行
     * @param urls
     * @param requestURI
     * @return
     */
    public boolean check(String[] urls,String requestURI){
        for (String url : urls) {
            boolean match = PATH_MATCHER.match(url, requestURI);
            if(match){
                return true;
            }
        }
        return false;
    }
}

AntPathMatcher 拓展:

介绍: Spring中提供的路径匹配器 ;

通配符规则:

符号含义
?匹配一个字符
*匹配0个或多个字符
**匹配0个或多个目录/字符

2). 开启组件扫描

需要在引导类上, 加上Servlet组件扫描的注解, 来扫描过滤器配置的@WebFilter注解, 扫描上之后, 过滤器在运行时就生效了。

@Slf4j
@SpringBootApplication
@ServletComponentScan
public class ReggieApplication {
    public static void main(String[] args) {
        SpringApplication.run(ReggieApplication.class,args);
        log.info("项目启动成功...");
    }
}

@ServletComponentScan 的作用:

​ 在SpringBoot项目中, 在引导类/配置类上加了该注解后, 会自动扫描项目中(当前包及其子包下)的@WebServlet , @WebFilter , @WebListener 注解, 自动注册Servlet的相关组件 ;

1.4 功能测试

当我们直接输入

http://localhost:8080/backend/login.html

会跳转到登录界面

http://localhost:8080/backend/page/login/login.html

视频的前端JS和我们实际的前端不同

代码编写完毕之后,我们需要将工程重启一下,然后在浏览器地址栏直接输入系统管理后台首页,然后看看是否可以跳转到登录页面即可。我们也可以通过debug的形式来跟踪一下代码执行的过程。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ncOlWYT3-1688460908063)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202304021802532.png)]

对于前端的代码, 也可以进行debug调试。

F12打开浏览器的调试工具, 找到我们前面提到的request.js, 在request.js的响应拦截器位置打上断点。

image-20210728001929657

2. 新增员工

2.1 需求分析

后台系统中可以管理员工信息,通过新增员工来添加后台系统用户。点击[添加员工]按钮跳转到新增页面,如下:

image-20210728002442334

当填写完表单信息, 点击"保存"按钮后, 会提交该表单的数据到服务端, 在服务端中需要接受数据, 然后将数据保存至数据库中。

2.2 数据模型

新增员工,其实就是将我们新增页面录入的员工数据插入到employee表。employee表中的status字段已经设置了默认值1,表示状态正常。

image-20210728004144521

需要注意,employee表中对username字段加入了唯一约束,因为username是员工的登录账号,必须是唯一的。

image-20210728004250254

2.3 程序执行流程

在开发代码之前,我们需要结合着前端页面发起的请求, 梳理一下整个程序的执行过程:

image-20210728005638224

A. 点击"保存"按钮, 页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端, 请求方式POST, 请求路径 /employee

B. 服务端Controller接收页面提交的数据并调用Service将数据进行保存

C. Service调用Mapper操作数据库,保存数据

2.4 代码实现

在EmployeeController中增加save方法, 用于保存用户员工信息。

A. 在新增员工时, 按钮页面原型中的需求描述, 需要给员工设置初始默认密码 123456, 并对密码进行MD5加密。

B. 在组装员工信息时, 还需要封装创建时间、修改时间,创建人、修改人信息(从session中获取当前登录用户)。

/*新增员工*/
     @PostMapping
    public R<String> save(@RequestBody Employee employee){
         log.info("新增员工,员工信息:{}",employee.toString());
        return  null;
    }

保证前端页面发送的请求 能够请求到save方法 并且提交的参数能够提交到 employee对象上

@RequestBody:用来获取客户端(前端 )提交上来的JSON字符串 并将ISON字符串转换成javabeen实体类

@RespomseBody:javabeen实体类转换为JSON

Employee:使用 Employee对象接收前端传递的请求参数(JSON格式)

R:返回值

总体解释1

这段代码是一个使用Spring框架的控制器方法,用于处理HTTP POST请求,并将请求主体中的数据映射为Employee对象来新增员工。

让我们逐行解释代码:

  • @PostMapping:这是一个注解,用于将该方法映射到HTTP的POST请求方法。它表示这个方法将处理来自客户端的POST请求。
  • public R<String> save(@RequestBody Employee employee):这是方法的声明。它接收一个Employee类型的参数,该参数将从请求主体中提取。@RequestBody注解告诉Spring框架从请求主体中获取JSON数据,并将其转换为Employee对象。
  • log.info("新增员工,员工信息:{}", employee.toString()):这行代码使用日志记录了新增员工的操作。log.info()方法用于打印信息,并使用占位符{}employee.toString()的返回值插入到日志信息中。这样可以在控制台或日志文件中查看新增员工的详细信息。
  • return null:这个方法返回一个null值。你可以根据实际需求来返回适当的响应,比如返回一个成功的消息或新员工的ID。

所以,当你发送一个HTTP POST请求到这个控制器的URL时,请求主体中的数据将被映射为Employee对象,并在日志中打印出员工的信息。然后,方法将返回一个null值作为响应。你可以根据需求修改方法体,执行适当的业务逻辑或返回相应的结果。

总体解释2

这段代码是一个基于SpringBoot框架搭建的员工管理系统的新增员工功能的接口。

其中,@PostMapping注解表示该接口只接受POST请求。@RequestBody注解表示将请求体中的JSON格式数据绑定到Employee对象上。Employee对象代表了一个员工信息,包括员工ID、姓名、性别、职位等属性。当接收到客户端传来的POST请求时,SpringBoot会自动将请求体中的JSON格式数据转换成对应的Employee对象,并赋值给方法参数employee。

在方法内部,使用log.info打印出新增的员工信息。最后,接口返回的类型为R,代表了一个自定义的响应类,其中R表示响应结果,而String则表示响应消息内容。在这个示例中,接口返回了null。需要根据实际业务需求将null替换成相应的响应结果。

总之,这段代码的主要作用是接收客户端提交的员工信息并写入数据库,同时记录日志并返回响应结果。

image-20230605105616707

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GkG6Hg4I-1688460908065)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306051054661.png)]

image-20230605105816824

image-20230605105845333

/**
 * 新增员工
 * @param employee
 * @return
 */
@PostMapping
public R<String> save(HttpServletRequest request,@RequestBody Employee employee){
    log.info("新增员工,员工信息:{}",employee.toString());

    //设置初始密码123456,需要进行md5加密处理
    employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
	
    employee.setCreateTime(LocalDateTime.now());
    employee.setUpdateTime(LocalDateTime.now());
	
    //获得当前登录用户的id
    Long empId = (Long) request.getSession().getAttribute("employee");

    employee.setCreateUser(empId);
    employee.setUpdateUser(empId);

    employeeService.save(employee);
    return R.success("新增员工成功");
}

代码解释1

这段代码是一个使用Spring框架的控制器方法,用于处理HTTP POST请求并新增员工。

让我们逐行解释代码:

  • @PostMapping:这是一个注解,将该方法映射到HTTP的POST请求方法。
  • public R<String> save(HttpServletRequest request, @RequestBody Employee employee):这是方法的声明。它接收两个参数:HttpServletRequest requestEmployee employee@RequestBody注解告诉Spring框架从请求主体中获取数据,并将其转换为Employee对象。HttpServletRequest参数用于获取当前请求的相关信息。
  • log.info("新增员工,员工信息:{}", employee.toString()):这行代码使用日志记录了新增员工的操作。员工信息通过employee.toString()转换为字符串,并插入到日志信息中。
  • employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes())):这行代码设置员工的初始密码为"123456",并使用MD5加密处理。DigestUtils.md5DigestAsHex()方法将字节数组转换为MD5散列字符串。
  • employee.setCreateTime(LocalDateTime.now()):这行代码将当前时间设置为员工的创建时间。
  • employee.setUpdateTime(LocalDateTime.now()):这行代码将当前时间设置为员工的更新时间。
  • Long empId = (Long) request.getSession().getAttribute("employee"):这行代码从当前会话中获取名为"employee"的属性,它表示当前登录用户的ID。
  • employee.setCreateUser(empId)employee.setUpdateUser(empId):这两行代码将当前登录用户的ID设置为员工的创建用户和更新用户。
  • employeeService.save(employee):这行代码调用employeeService服务的save()方法来保存新增的员工信息。
  • return R.success("新增员工成功"):这行代码返回一个成功的响应,包含一条"新增员工成功"的消息。

总结起来,这段代码处理了一个HTTP POST请求,将请求主体中的数据映射为Employee对象,并对该对象进行一系列处理,如设置初始密码、设置创建时间和更新时间、设置创建用户和更新用户等。最后,调用employeeServicesave()方法来保存员工信息,并返回成功的响应消息。

代码解释2

这段代码是一个基于SpringBoot框架搭建的员工管理系统的新增员工功能的接口,相对于之前的代码增加了一些业务逻辑的处理。

其中,@PostMapping注解表示该接口只接受POST请求。@RequestBody注解表示将请求体中的JSON格式数据绑定到Employee对象上。HttpServletRequest request代表HTTP请求信息。Employee对象代表了一个员工信息,包括员工ID、姓名、性别、职位等属性。

在方法内部,使用log.info打印出新增的员工信息。接下来对员工对象进行了一些预处理操作:

  1. 设置初始密码123456,并通过MD5加密后存入employee对象中;
  2. 获取当前时间,并分别设置为创建时间和更新时间;
  3. 从session中获取当前登录用户的id,并将其设置为创建用户和更新用户;

最后,调用employeeService.save()方法将员工信息写入数据库中,并在接口返回时返回一个自定义的响应类R,其中success表示成功的状态,"新增员工成功"表示响应消息的内容。

总之,这段代码的主要作用是接收客户端提交的员工信息并写入数据库,并进行了一些预处理操作。同时记录日志并返回响应结果。

测试新增员工

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TLxlqAru-1688460908067)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131535557.png)]

image-20230605112841758

image-20230605112910313

如果在录入一个zhagsan 就会出现问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sYFBDM4l-1688460908070)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131535685.png)]

image-20230605113648946

image-20230605113727456

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oakWy0Xb-1688460908072)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306052104630.png)]

2.5 功能测试

代码编写完毕之后,我们需要将工程重启, 完毕之后直接访问管理系统首页, 点击 “员工管理” 页面中的 “添加员工” 按钮, 输入员工基本信息, 然后点击 “保存” 进行数据保存, 保存完毕后, 检查数据库中是否录入员工数据。

当我们在测试中,添加用户时, 输入了一个已存在的用户名时,前端界面出现错误提示信息:

image-20210728010841569

而此时,服务端已经报错了, 报错信息如下:

image-20210728010938086

出现上述的错误, 主要就是因为在 employee 表结构中,我们针对于username字段,建立了唯一索引,添加重复的username数据时,违背该约束,就会报错。但是此时前端提示的信息并不具体,用户并不知道是因为什么原因造成的该异常,我们需要给用户提示详细的错误信息 。

2.6 全局异常处理

2.6.1 思路分析

要想解决上述测试中存在的问题,我们需要对程序中可能出现的异常进行捕获,通常有两种处理方式:

A. 在Controller方法中加入 try…catch 进行异常捕获

形式如下:

image-20210729094125294

如果采用这种方式,虽然可以解决,但是存在弊端,需要我们在保存其他业务数据时,也需要在Controller方法中加上try…catch进行处理,代码冗余,不通用。

B. 使用异常处理器进行全局异常捕获

采用这种方式来实现,我们只需要在项目中定义一个通用的全局异常处理器,就可以解决本项目的所有异常。

2.6.2 全局异常处理器

在项目中自定义一个全局异常处理器,在异常处理器上加上注解 @ControllerAdvice,可以通过属性annotations指定拦截哪一类的Controller方法。 并在异常处理器的方法上加上注解 @ExceptionHandler 来指定拦截的是那一类型的异常。

异常处理方法逻辑:

  • 指定捕获的异常类型为 SQLIntegrityConstraintViolationException
  • 解析异常的提示信息, 获取出是那个值违背了唯一约束
  • 组装错误信息并返回
image-20210729100232642
/**
 * 全局异常处理
 */
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 异常处理方法
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
        log.error(ex.getMessage());
         return R.error("失败了");
    }
}

代码解释1:

以上代码是一个用于全局异常处理的类。该类使用了Spring的@ControllerAdvice注解来标识它是一个全局异常处理器。通过该注解,它会拦截被@RestController和@Controller注解标注的类中发生的异常。

具体解释如下:

  1. @ControllerAdvice(annotations = {RestController.class, Controller.class}):这是一个类级别的注解,用于指定该全局异常处理类要拦截的Controller类的注解类型。在这里,它指定要拦截的类必须是标注了@RestController或@Controller注解的类。
  2. @ResponseBody:这是一个方法级别的注解,表示该方法的返回值将直接作为响应的内容返回给客户端,而不是被视图解析器解析为视图。在这里,它确保异常处理方法的返回值将被转换为响应的JSON格式。
  3. @ExceptionHandler(SQLIntegrityConstraintViolationException.class):这是一个方法级别的注解,用于指定该方法要处理的特定异常类型。在这里,它指定该方法要处理的异常是SQLIntegrityConstraintViolationException。
  4. public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex):这是异常处理方法,它接收一个SQLIntegrityConstraintViolationException类型的参数,表示捕获到的异常对象。方法的返回类型为R,R是一个自定义的通用响应对象,用于包装响应数据。
  5. log.error(ex.getMessage()):这行代码使用Slf4j提供的日志记录功能,将异常的错误消息记录到日志中。
  6. return R.error("失败了"):这行代码返回一个R对象,表示处理异常后的响应结果。使用R.error静态方法创建一个错误类型的R对象,并设置错误消息为"失败了"。

综上所述,以上代码定义了一个全局异常处理类,用于捕获和处理Controller层中发生的SQLIntegrityConstraintViolationException异常,并将错误信息记录到日志中,最后返回一个错误类型的响应结果。

代码解释2

这段代码是一个基于SpringBoot框架的全局异常处理类,用于捕获并处理SQL异常。

@ControllerAdvice注解表示该类用于全局异常处理。通过annotations参数指定了需要拦截的控制器注解类型为@RestController和@Controller。@ResponseBody注解表示返回的是JSON格式的数据。@Slf4j注解表示使用log记录日志。

@ExceptionHandler注解表示该方法用于捕获SQLIntegrityConstraintViolationException类型的异常。当系统出现SQLIntegrityConstraintViolationException类型的异常时,会调用该方法进行异常处理。在方法内部,使用log.error打印异常信息,并返回一个自定义的响应类R,其中error表示失败的状态,"失败了"表示响应消息的内容。

总之,这段代码的主要作用是实现对SQLIntegrityConstraintViolationException类型异常的统一捕获和处理。当系统出现该类型的异常时,会执行该方法进行异常处理,并记录日志并返回响应结果。

image-20230605205207722

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oGQEzmmS-1688460908073)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131536773.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zDnZxiTx-1688460908074)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131536460.png)]

image-20230605213228144

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m2WjK5TB-1688460908076)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131536013.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gT3qFPmr-1688460908076)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131536837.png)]

所属包: com.itheima.reggie.common

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.sql.SQLIntegrityConstraintViolationException;

/**
 * 全局异常处理
 */
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class  GlobalExceptionHandler {

    /**
     * 异常处理方法
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
        log.error(ex.getMessage());
        if(ex.getMessage().contains("Duplicate entry")){
            String[] split = ex.getMessage().split(" ");
            String msg = split[2] + "已存在";
            return R.error(msg);
        }
        return R.error("未知错误");
    }
}

注解说明:

​ 上述的全局异常处理器上使用了的两个注解 @ControllerAdvice , @ResponseBody , 他们的作用分别为:

​ @ControllerAdvice : 指定拦截那些类型的控制器;

​ @ResponseBody: 将方法的返回值 R 对象转换为json格式的数据, 响应给页面;

​ 上述使用的两个注解, 也可以合并成为一个注解 @RestControllerAdvice

image-20210729100052940

image-20230605214040718

代码解释1

以上代码是一个 Java 类,它包含了一个名为GlobalExceptionHandler的异常处理类。该类包含了一个名为exceptionHandler的方法,该方法接收一个SQLIntegrityConstraintViolationException类型的异常作为参数。

exceptionHandler方法中,首先使用log.error(ex.getMessage())方法将异常消息保存到日志中。然后,使用ex.getMessage()方法获取异常消息,并将其分成多个字符串,以便进一步处理。

如果异常消息中包含字符串"Duplicate entry",则方法将使用split()方法将字符串分成多个字符串,然后从中获取第 3 个字符串,该字符串通常是描述存在重复项的详细信息。最后,方法将使用error()方法将详细信息转换为R<String>类型的响应,并返回该响应。如果异常消息中包含其他字符串,则方法将返回R.error("未知错误")类型的响应。

需要注意的是,该代码中的@ExceptionHandler注解用于指定处理的异常类型,并返回R<String>类型的数据。该注解需要与@RestController注解一起使用,以便将异常处理与 RESTful API 集成。

在详细解释

以上代码中的@ExceptionHandler注解是一个异常处理注解,它用于指定处理哪种类型的异常。在此示例中,@ExceptionHandler(SQLIntegrityConstraintViolationException.class)指定了该方法处理SQLIntegrityConstraintViolationException类型的异常。

@ExceptionHandler注解有一个参数,即要处理的异常类型。在此示例中,SQLIntegrityConstraintViolationException是我们要处理的异常类型。当程序遇到这种异常时,它将被传递给exceptionHandler方法进行处理。

exceptionHandler方法是一个返回值方法,它接收一个SQLIntegrityConstraintViolationException类型的异常作为参数。在方法内部,我们首先使用log.error(ex.getMessage())方法将异常消息保存到日志中。然后,我们使用ex.getMessage()方法获取异常消息,并将其分成多个字符串。

如果异常消息中包含字符串"Duplicate entry",则方法将使用split()方法将字符串分成多个字符串,然后从中获取第 3 个字符串,该字符串通常是描述存在重复项的详细信息。在此示例中,我们将第 3 个字符串作为参数传递给error()方法,以将详细信息转换为R<String>类型的响应。如果异常消息中包含其他字符串,则方法将返回R.error("未知错误")类型的响应。

最后,我们需要将异常处理与 RESTful API 集成。在此示例中,@RestController注解指定了该控制器处理 RESTful API 请求。因此,当程序遇到异常时,它将被传递给 RESTful API 控制器中的异常处理函数。在这种情况下,该函数使用@ExceptionHandler注解指定要处理的异常类型,并返回一个错误响应。

代码解释2

以上代码是一个全局异常处理器,用于捕获在@RestController 或 @Controller标注的控制器中抛出的异常,并返回统一格式的错误信息。

其中,@ControllerAdvice 注解表示该类是一个全局异常处理器,@ResponseBody 注解表示返回结果是一个 JSON 格式的数据,@Slf4j 则是一个Slf4j日志工具,常用于输出日志信息。

在代码实现方面,@ExceptionHandler 注解用于指定处理的异常类型,并返回 R 类型的数据。

这段代码的处理流程是:当捕获到 SQLIntegrityConstraintViolationException 异常时,如果异常信息中包含"Duplicate entry"字样,则认为是数据重复的异常,从异常信息中提取出具体的数据,然后返回一个包含该信息的 R 类型的数据;否则,返回一个"未知错误"的 R 类型的数据。

代码解释3

以上代码是一个全局异常处理类,使用了Spring框架提供的@ControllerAdvice注解。该类用于捕获和处理控制器中抛出的异常。

代码中的注解@ControllerAdvice(annotations = {RestController.class, Controller.class})表示该类只会处理带有@RestController@Controller注解的控制器中发生的异常。

注解@ResponseBody表示该类的方法返回的结果将直接作为响应体返回,而不是被视图解析器解析为视图。就是将javabeen转换成JSON返回

@Slf4j是一个Lombok注解,用于自动生成日志记录器。

@ExceptionHandler(SQLIntegrityConstraintViolationException.class)表示该方法将处理SQLIntegrityConstraintViolationException类型的异常。在方法中,它首先记录了异常的错误消息,然后检查错误消息中是否包含"Duplicate entry",如果包含,则根据错误消息中的内容生成相应的错误信息并返回一个R对象(可能是自定义的响应对象)。

如果错误消息不包含"Duplicate entry",则返回一个包含默认错误信息的R对象。

值得注意的是,代码中的返回 R.error(msg);这行代码是伪代码,实际上应该返回一个R对象。在这个例子中,R.error(msg)是一个伪代码,意思是返回一个带有错误信息msgR对象。具体的R对象实现和使用方式需要根据实际情况进行定义和处理。

总结:以上代码是一个全局异常处理类,用于捕获和处理控制器中抛出的SQLIntegrityConstraintViolationException异常。根据异常的错误消息,它生成相应的错误信息并返回一个R对象(伪代码),用于表示异常处理结果。

在详细一点解释

当在使用Spring框架进行开发时,我们经常需要处理控制器中可能抛出的异常,以提供更好的错误处理和友好的错误响应。全局异常处理类就是用于这个目的。

首先,我们可以使用@ControllerAdvice注解来标识一个类作为全局异常处理类。在本例中,该注解使用了annotations属性,指定了需要处理的控制器注解类型为@RestController@Controller

接下来,使用@ExceptionHandler注解来标识一个方法作为异常处理方法。在本例中,该方法处理的是SQLIntegrityConstraintViolationException类型的异常。

在异常处理方法中,我们首先使用@Slf4j注解生成一个日志记录器,以便记录异常的错误消息。

接下来,我们检查异常的错误消息是否包含"Duplicate entry",即是否是因为违反了唯一性约束(Duplicate entry)而抛出的异常。如果是,我们从错误消息中提取出相应的信息,并根据该信息生成一个新的错误消息。

在这个例子中,我们使用了ex.getMessage().contains("Duplicate entry")来检查错误消息是否包含"Duplicate entry",然后使用ex.getMessage().split(" ")来将错误消息按空格分割,获取了相关信息。

生成新的错误消息后,我们使用R.error(msg)来创建一个响应对象,其中msg是错误消息。这里的R.error()是一个伪代码,表示返回一个带有错误信息的响应对象。

如果错误消息不包含"Duplicate entry",则说明是其他未知错误,我们返回一个默认的错误响应对象,其中包含一个表示未知错误的默认错误信息。

需要注意的是,代码中的返回 R.error(msg);这行代码是伪代码,实际上应该返回一个R对象,具体的返回方式需要根据实际情况进行定义和处理。

通过使用这个全局异常处理类,我们可以集中处理控制器中抛出的指定类型的异常,并提供自定义的错误响应。这样可以简化代码逻辑,并提供统一的异常处理机制,提升系统的健壮性和用户体验。

当我们新增一个已经存在的值的时候

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yYdGR7gY-1688460908077)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131537709.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LPG10VAA-1688460908078)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131537244.png)]

当我们新增不存在的值的时候

image-20230605220456101

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZxaVZMON-1688460908079)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131537941.png)]

image-20230605220526780

2.6.3 测试

全局异常处理器编写完毕之后,我们需要将项目重启, 完毕之后直接访问管理系统首页, 点击 “员工管理” 页面中的 “添加员工” 按钮。当我们在测试中,添加用户时, 输入了一个已存在的用户名时,前端界面出现如下错误提示信息:

image-20210729102220135

image-20230605220945408

3. 员工分页查询

3.1 需求分析

系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。而在我们的分页查询页面中, 除了分页条件以外,还有一个查询条件 比如查询"员工姓名"。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dgn6vaVn-1688460908081)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202304021803131.png)]

  • 请求参数

    • 搜索条件: 员工姓名(模糊查询)

    • 分页条件: 每页展示条数 , 页码

  • 响应数据

    • 总记录数
  • 结果列表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gbQrUaTx-1688460908082)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131538518.png)]

1). 页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端

F12 刷新页面

image-20230605221857857

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kXxtcVB6-1688460908083)(C:\Users\zyz\AppData\Roaming\Typora\typora-user-images\image-20230605221909418.png)]

page=1&pageSize=10 查第一页 一共查10条数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p1zw7eSO-1688460908084)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131538788.png)]

看前端代码

image-20230605222232623

为什么一加载 就会把请求发出去

 <script>
      new Vue({
        el: '#member-app',
        data() {
          return {
             input: '',
             counts: 0,
             page: 1,
             pageSize: 2,
             tableData : [],
             id : '',
             status : '',
          }
        },
        computed: {},
        created() {
          this.init()
            if(localStorage.getItem('userInfo') != null){
                //获取当前登录员工的账号,并赋值给模型数据user
                this.user = JSON.parse(localStorage.getItem('userInfo')).username
            }
        },
        mounted() {
        },
        methods: {
          async init () {
            const params = {
              page: this.page,
              pageSize: this.pageSize,
              name: this.input ? this.input : undefined
            }
            await getMemberList(params).then(res => {
              if (String(res.code) === '1') {
                this.tableData = res.data.records || []
                this.counts = res.data.total
              }
            }).catch(err => {
              this.$message.error('请求出错了:' + err)
            })
          },
          handleQuery() {
            this.page = 1;
            this.init();
          },
           // 添加
          addMemberHandle (st) {
            if (st === 'add'){
              window.parent.menuHandle({
                id: '2',
                url: '/backend/page/member/add.html',
                name: '添加员工'
              },true)
            } else {
              window.parent.menuHandle({
                id: '2',
                url: '/backend/page/member/add.html?id='+st,
                name: '修改员工'
              },true)
            }
          },
          //状态修改
          statusHandle (row) {
            this.id = row.id
            this.status = row.status
            this.$confirm('确认调整该账号的状态?', '提示', {
              'confirmButtonText': '确定',
              'cancelButtonText': '取消',
              'type': 'warning'
              }).then(() => {
              enableOrDisableEmployee({ 'id': this.id, 'status': !this.status ? 1 : 0 }).then(res => {
                console.log('enableOrDisableEmployee',res)
                if (String(res.code) === '1') {
                  this.$message.success('账号状态更改成功!')
                  this.handleQuery()
                }
              }).catch(err => {
                this.$message.error('请求出错了:' + err)
              })
            })
          },
          handleSizeChange (val) {
            this.pageSize = val
            this.init()
          },
          handleCurrentChange (val) {
            this.page = val
            this.init()
          }
        }
      })
    </script>

以上代码是一个使用Vue框架编写的前端页面脚本。它创建了一个Vue实例,其中包含了一些数据和方法。

首先,在Vue实例的data属性中定义了一些数据,包括input(输入框的值),counts(数据总数),page(当前页码),pageSize(每页显示的数据量),tableData(表格数据),id(员工ID),status(员工状态)等。

created生命周期钩子函数中,通过读取浏览器本地存储(localStorage)中的userInfo,将当前登录员工的账号赋值给user数据。

mounted生命周期钩子函数中,可以执行一些页面加载后需要进行的操作,但在这个代码片段中没有具体的逻辑。

接下来是一些方法的定义:

  1. init方法:该方法用于初始化页面数据,它通过调用getMemberList函数获取员工列表数据,并将返回的数据赋值给tableDatacounts
  2. handleQuery方法:当用户点击查询按钮时,将页码(page)重置为1,然后调用init方法重新加载数据。
  3. addMemberHandle方法:用于处理添加员工或修改员工的操作。根据传入的参数st(可以是’add’或员工ID),通过调用window.parent.menuHandle方法打开相应的页面进行添加或修改员工操作。
  4. statusHandle方法:用于处理账号状态的更改。首先将员工的ID和状态赋值给idstatus,然后弹出确认对话框,确认调整账号状态后,调用enableOrDisableEmployee函数发送请求,将员工的状态进行更改。如果请求成功,则显示成功提示信息,并调用handleQuery方法重新加载数据。
  5. handleSizeChange方法:当用户改变每页显示数据量时,将新的数据量赋值给pageSize,然后调用init方法重新加载数据。
  6. handleCurrentChange方法:当用户改变当前页码时,将新的页码赋值给page,然后调用init方法重新加载数据。

综上所述,以上代码主要实现了一个员工管理的前端页面,包括员工列表的展示、查询、添加、修改和账号状态的更改等功能。

function getMemberList (params) {
  return $axios({
    url: '/employee/page',
    method: 'get',
    params
  })
}

以上代码定义了一个名为getMemberList的函数,它接受一个名为params的参数。

该函数使用$axios对象(可能是一个封装了Ajax请求的库)发送一个GET请求,请求的URL是/employee/page。请求的参数使用params对象传递,并设置请求的方法为GET。

函数执行后,会返回一个Promise对象,可以通过.then().catch()方法处理请求的结果或错误。

简而言之,该函数用于发送一个GET请求到指定URL,参数为params对象中的参数,以获取员工列表的数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d7wxrkJk-1688460908085)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131538116.png)]

3.2 程序执行流程

3.2.1 页面流程分析

在开发代码之前,需要梳理一下整个程序的执行过程。

A. 点击菜单,打开员工管理页面时,执行查询:

image-20210729163400772

B. 搜索栏输入员工姓名,回车,执行查询:

image-20210729164259997

1). 页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端

2). 服务端Controller接收页面提交的数据, 并组装条件调用Service查询数据

3). Service调用Mapper操作数据库,查询分页数据

4). Controller将查询到的分页数据, 响应给前端页面

5). 页面接收到分页数据, 并通过ElementUI的Table组件展示到页面上

3.2.2 前端代码介绍

1). 访问员工列表页面/member/list.html时, 会触发Vuejs中的钩子方法, 在页面初始化时调用created方法

image-20210729231639034

从上述的前端代码中我们可以看到, 执行完分页查询, 我们需要给前端返回的信息中需要包含两项 : records 中封装结果列表, total中封装总记录数 。

而在组装请求参数时 , page、pageSize 都是前端分页插件渲染时的参数;

image-20210729232916380

2). 在getMemberList方法中, 通过axios发起异步请求

image-20210729231745143

axios发起的异步请求会被声明在 request.js 中的request拦截器拦截, 在其中对get请求进行进一步的封装处理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sBOP16Mi-1688460908087)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202304021804051.png)]

最终发送给服务端的请求为 : GET请求 , 请求链接 /employee/page?page=1&pageSize=10&name=xxx

3.3 代码实现

3.3.1 分页插件配置

当前我们要实现的分页查询功能,而在MybatisPlus要实现分页功能,就需要用到MybatisPlus中提供的分页插件,要使用分页插件,就要在配置类中声明分页插件的bean对象。

所属包: com.itheima.reggie.config

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 配置MP的分页插件
 */
@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        //创建了一个`MybatisPlusInterceptor`对象
        //`MybatisPlusInterceptor`是MyBatis-Plus框架提供的拦截器,可以在执行MyBatis的SQL语句过程中添加额外的功能。
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        //通过调用`addInnerInterceptor`方法,将一个`PaginationInnerInterceptor`对象作为内部拦截器添加到`MybatisPlusInterceptor`中。`PaginationInnerInterceptor`是MyBatis-Plus提供的分页插件,它能够拦截SQL语句,并根据传入的分页参数自动进行分页查询。
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

代码解释1

以上代码是一个Java类,用于配置MyBatis-Plus(简称MP)的分页插件。

  1. @Configuration注解表示这是一个配置类,用于配置应用程序的一些组件或功能。
  2. MybatisPlusConfig类中定义了一个名为mybatisPlusInterceptor的Bean方法,用于创建并返回一个MybatisPlusInterceptor对象。
  3. MybatisPlusInterceptor是MyBatis-Plus提供的拦截器,用于拦截MyBatis的SQL执行过程,在执行过程中添加额外的功能。通过调用addInnerInterceptor方法,将一个PaginationInnerInterceptor对象添加为MybatisPlusInterceptor的内部拦截器。
  4. PaginationInnerInterceptor是MyBatis-Plus提供的分页插件,它可以自动拦截SQL语句,根据传入的分页参数进行分页查询。将PaginationInnerInterceptor添加到MybatisPlusInterceptor中后,可以实现对查询结果进行自动分页。
  5. @Bean注解表示将该方法返回的对象注册为一个Spring的Bean,可以在其他组件中通过依赖注入来使用。

综上所述,以上代码配置了MyBatis-Plus的分页插件,通过创建MybatisPlusInterceptor对象,并添加PaginationInnerInterceptor作为内部拦截器,实现了对MyBatis的自动分页功能。

详细一点

当使用MyBatis-Plus框架进行数据库操作时,有时需要对查询结果进行分页显示。以上代码展示了如何配置MyBatis-Plus的分页插件。

首先,创建一个Java类,并添加@Configuration注解,表示这是一个配置类。该类的主要目的是配置MyBatis-Plus的分页插件。

在该配置类中,定义了一个名为mybatisPlusInterceptor的方法,并使用@Bean注解进行标记。这意味着该方法返回的对象会被注册为一个Spring的Bean,可以在其他组件中通过依赖注入来使用。

mybatisPlusInterceptor方法中,创建了一个MybatisPlusInterceptor对象,并进行了配置。MybatisPlusInterceptor是MyBatis-Plus框架提供的拦截器,可以在执行MyBatis的SQL语句过程中添加额外的功能。

通过调用addInnerInterceptor方法,将一个PaginationInnerInterceptor对象作为内部拦截器添加到MybatisPlusInterceptor中。PaginationInnerInterceptor是MyBatis-Plus提供的分页插件,它能够拦截SQL语句,并根据传入的分页参数自动进行分页查询。

这样,通过配置MybatisPlusInterceptor的内部拦截器为PaginationInnerInterceptor,就实现了对MyBatis的自动分页功能。

总结起来,以上代码通过配置MyBatis-Plus的分页插件,使得在使用MyBatis-Plus进行数据库查询时,可以方便地进行分页操作,以便在页面上分批显示查询结果。这样,开发人员无需手动编写分页逻辑,而是让分页插件自动拦截SQL语句,并根据传入的分页参数进行分页查询,简化了开发过程。

代码解释2

这段代码实现了MyBatis Plus分页插件的配置。MyBatis Plus是基于MyBatis的增强工具,在提升开发效率的同时也提供了一些强大的功能,其中之一就是分页插件。

在这段代码中,我们定义了一个MybatisPlusInterceptor实例,并向其中添加了一个PaginationInnerInterceptor内部拦截器,该拦截器就是用于实现分页功能的。最后将该MybatisPlusInterceptor实例作为Bean注册到Spring容器中,完成MyBatis Plus分页插件的配置。

代码解释3

以上代码是一个 MyBatis-Plus 的配置类,它定义了一个名为mybatisPlusInterceptor的 Bean。该 Bean 的作用是在 MyBatis-Plus 中使用 Pagination 插件进行分页处理。

mybatisPlusInterceptor方法中,我们创建了一个名为MybatisPlusInterceptor的新对象,并将其返回。这个对象是一个拦截器对象,可以在执行 SQL 操作之前或之后进行拦截。在这里,我们添加了一个内部拦截器PaginationInnerInterceptor,以便在执行查询操作时进行分页处理。

PaginationInnerInterceptor是一个自定义的内部拦截器,它的作用是在执行查询操作时,将查询语句分成多个部分,并根据需要对每个部分进行分页处理。在这个例子中,我们将每个部分的大小设置为 10,这意味着每个部分的大小为 10 条记录。

在返回MybatisPlusInterceptor对象之后,我们可以在配置文件中定义@Bean方法来创建该对象。在这种情况下,MybatisPlusConfig类是一个配置类,它定义了一个@Bean方法mybatisPlusInterceptor来创建MybatisPlusInterceptor对象。

通过在配置文件中定义@Bean方法来创建MybatisPlusInterceptor对象,我们可以在应用程序启动时自动配置 MyBatis-Plus,以便在执行查询操作时进行分页处理。

详细一点

MyBatis-Plus 是一个开源的持久层框架,它提供了许多功能,例如自动建表、动态 SQL、事务管理等等。其中,分页插件是 MyBatis-Plus 提供的一个重要功能,它可以帮助开发人员更方便地进行分页处理。

以上代码中的 MybatisPlusConfig 类是一个配置类,它定义了一个 @Bean 方法 mybatisPlusInterceptor,用于创建 MybatisPlusInterceptor 对象。这个对象是一个拦截器对象,可以在执行 SQL 操作之前或之后进行拦截。在这里,我们添加了一个内部拦截器 PaginationInnerInterceptor,它的作用是在执行查询操作时,将查询语句分成多个部分,并根据需要对每个部分进行分页处理。

PaginationInnerInterceptor 是自定义的内部拦截器,它继承了 MyBatisPlusInterceptor 类。MyBatisPlusInterceptor 类是 MyBatis-Plus 提供的默认拦截器类,它提供了一些基本的拦截功能,例如 SQL 过滤、事务管理、异常处理等等。在 PaginationInnerInterceptor 类中,我们重写了其中的一些方法,例如 beforeQuery,用于在查询之前进行分页处理。

beforeQuery 方法中,我们使用 String.split() 方法将查询语句分成多个部分,并将每个部分作为参数传递给 MyBatisPlusInterceptorbeforeQuery 方法。在 beforeQuery 方法中,我们可以使用 MapperScannerConfigurer 类来扫描和配置 Mapper 接口,以便在查询时执行相应的操作。

通过以上代码,我们可以在 MyBatis-Plus 中实现分页功能,而不需要修改 MyBatis-Plus 的源代码。同时,我们还可以在配置文件中定义 @Bean 方法来创建 MybatisPlusInterceptor 对象,以便在应用程序启动时自动配置 MyBatis-Plus,从而提高系统的可维护性和可扩展性。

3.3.2 分页查询实现

在上面我们已经分析了,页面在进行分页查询时, 具体的请求信息如下:

请求说明
请求方式GET
请求路径/employee/page
请求参数page , pageSize , name

那么查询完毕后我们需要给前端返回什么样的结果呢?

在上述我们也分析了, 查询返回的结果数据data中应该封装两项信息, 分别为: records 封装分页列表数据, total 中封装符合条件的总记录数。 那么这个时候, 在定义controller方法的返回值类型R时, 我们可以直接将 MybatisPlus 分页查询的结果 Page 直接封装返回, 因为Page中的属性如下:

image-20210729235403154

那么接下来就依据于这些已知的需求和条件完成分页查询的代码实现。 具体的逻辑如下:

A. 构造分页条件

B. 构建搜索条件 - name进行模糊匹配

C. 构建排序条件 - 更新时间倒序排序

D. 执行查询

E. 组装结果并返回

records   结果列表
total     总记录数   

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CXUtqRG7-1688460908088)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131539429.png)]

image-20230606082032620

接收服务端的响应

   await getMemberList(params).then(res => {
              if (String(res.code) === '1') {
                this.tableData = res.data.records || []
                this.counts = res.data.total
              }
            }

服务端响应回来的数据

 this.tableData = res.data.records || []
 this.counts = res.data.total

这就说明 我们响应的JSON里面 应该有records total字段 而我们的Employee没有这二个字段 所以我们在EmployeeController里面使用的泛型应该是Page Page是MybatisPlus给我们封装的一个类 一会我们进行分页查询 最终也会返回这个对象 如下图

在我们的Page里面有我们的.records和total属性 .records放着是当前页 展示的列表数据 total 总记录数 总条数 所以这里面为啥要使用Page 就是因为页面上要拿数据 records和total

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hzsRlDjw-1688460908089)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131539517.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7cVWiVjr-1688460908090)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131540157.png)]

  public  R<Page> page() {

         return  null;

      }

image-20230606083713938

  /**
     * 员工信息分页查询
     * @param page 当前查询页码
     * @param pageSize 每页展示记录数
     * @param name 员工姓名 - 可选参数
     * @return
     */
    @GetMapping("/page")
      public  R<Page> page(int page ,int pageSize, String name) {
        log.info("page = {},pageSize = {},name = {}" ,page,pageSize,name);
         return  null;
      }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jryzYHDC-1688460908091)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131540813.png)]

刷新 页面 并登录

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lgQ7Anmi-1688460908092)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131540365.png)]

F8

image-20230606084530751

放行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3yuxSMuq-1688460908092)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131540621.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nvr0TdpU-1688460908093)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131540085.png)]

image-20230606084723825

具体的代码实现如下:

/**
 * 员工信息分页查询
 * @param page 当前查询页码
 * @param pageSize 每页展示记录数
 * @param name 员工姓名 - 可选参数
 * @return
 */
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
    log.info("page = {},pageSize = {},name = {}" ,page,pageSize,name);
    //构造分页构造器
    Page pageInfo = new Page(page,pageSize);

    //构造条件构造器
    LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();
    //添加过滤条件
    queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
    //添加排序条件
    queryWrapper.orderByDesc(Employee::getUpdateTime);

    //执行查询
    employeeService.page(pageInfo,queryWrapper);
    return R.success(pageInfo);
}

代码解释1

以上代码是一个Java Spring Boot中的员工信息分页查询的接口方法。下面是对代码的解释:

  1. @GetMapping("/page"): 这是一个GET请求的映射路径,用于处理分页查询请求。请求路径为/page
  2. public R<Page> page(int page, int pageSize, String name): 这是一个公共方法,返回类型为R<Page>,即包装了响应结果的对象。方法名为page,接受三个参数:page表示当前查询的页码,pageSize表示每页展示的记录数,name表示员工姓名(可选参数)。
  3. log.info("page = {},pageSize = {},name = {}", page, pageSize, name): 这行代码用于输出日志,记录传入的参数值。
  4. Page pageInfo = new Page(page, pageSize): 这是一个分页构造器,用于创建一个分页对象,传入当前查询的页码和每页展示的记录数。
  5. LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper(): 这是一个条件构造器,用于构建查询条件的对象。
  6. queryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name): 这行代码添加了一个模糊查询条件,根据员工姓名(Employee对象的name属性)进行模糊匹配。StringUtils.isNotEmpty(name)用于判断name参数是否非空,如果非空,则添加模糊查询条件;否则不添加该条件。
  7. queryWrapper.orderByDesc(Employee::getUpdateTime): 这行代码添加了一个排序条件,根据员工的更新时间(Employee对象的updateTime属性)进行降序排序。
  8. employeeService.page(pageInfo, queryWrapper): 这行代码执行查询操作,调用employeeService中的page方法,传入分页对象和查询条件对象,将查询结果封装到pageInfo对象中。
  9. return R.success(pageInfo): 最后,将封装了查询结果的pageInfo对象作为响应结果,通过R.success()方法包装后返回。

总体而言,该接口方法接受分页查询的参数,根据参数构建查询条件,并执行查询操作,返回包含查询结果的分页对象。

这里面的StringUtils是什么

StringUtils是一个字符串处理工具类,通常是Apache Commons Lang库中的一个类。它提供了许多用于处理字符串的实用方法,例如判断字符串是否为空、去除字符串中的空格、比较字符串等。

在代码中,StringUtils.isNotEmpty(name)用于判断name参数是否为空。如果name不为空,则返回true;如果name为空或为null,则返回false。这个判断条件用于决定是否向查询条件中添加员工姓名的模糊查询条件。

需要注意的是,StringUtils类可以有不同的实现,而具体使用哪个实现取决于项目中引入的库和版本。

代码解释2

以上代码是一个简单的员工信息分页查询的 Spring Boot 应用程序,其功能是接收三个参数:当前查询页码 page、每页展示记录数 pageSize 和员工姓名 name,并返回一个成功响应 PageInfo 对象。

以下是对代码的详细解释:

  1. @GetMapping("/page") 注解指定了请求路径为 /page,并映射到 page 方法上。
  2. log.info("page = {},pageSize = {},name = {}" ,page,pageSize,name) 方法中,使用了 log.info 方法输出了三个参数的值,分别是当前查询页码 page、每页展示记录数 pageSize 和员工姓名 name。
  3. Page pageInfo = new Page(page,pageSize) 创建了一个 PageInfo 对象 pageInfo,该对象用于封装分页信息和查询条件。其中,page 参数指定了当前查询页码,pageSize 参数指定了每页展示记录数。
  4. LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper() 创建了一个 LambdaQueryWrapper 对象 queryWrapper,该对象用于封装查询条件。其中,Employee 是员工信息类的泛型类型,name 参数指定了要查询的员工姓名。
  5. queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name) 添加了一个过滤条件,即对员工姓名进行模糊匹配。其中,StringUtils.isNotEmpty(name) 方法检查了 name 参数是否不为空,Employee::getName 方法引用了员工信息的类方法 getName,用于返回员工姓名。
  6. queryWrapper.orderByDesc(Employee::getUpdateTime) 添加了一个排序条件,即按照员工更新时间进行降序排序。
  7. employeeService.page(pageInfo,queryWrapper) 调用了 employeeService 的 page 方法,该方法接收两个参数:pageInfo 对象和 LambdaQueryWrapper 对象。pageInfo 对象用于封装分页信息和查询条件,LambdaQueryWrapper 对象用于封装查询条件。
  8. return R.success(pageInfo) 返回了一个成功响应对象 R,其中包含了分页信息和查询结果。

综上所述,以上代码实现了一个简单的员工信息分页查询功能,接收三个参数:当前查询页码 page、每页展示记录数 pageSize 和员工姓名 name,并返回一个成功响应 PageInfo 对象。

代码解释3

以上代码是一个 Java SpringBoot 项目中的员工信息分页查询 API 接口,主要用于查询符合指定条件的员工列表,并分页展示。具体解释如下:

  1. @GetMapping(“/page”) 注解表示这个接口处理 GET 请求,并注册了 /page 的路径映射。
  2. page、pageSize 和 name 这三个参数分别代表当前查询页码、每页展示记录数和员工姓名(可选),这些参数将作为接口的请求参数传入。
  3. Page pageInfo = new Page(page, pageSize) 给 pageInfo 对象赋值,page 是当前页码,pageSize 是每页展示记录数,这行代码构造了一个分页构造器对象并传入这两个参数,再将其赋值给 pageInfo。
  4. LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() 构造一个条件构造器,用于构建 SQL 查询语句。
  5. queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name) 给 queryWrapper 添加条件过滤器,如果 name 不为空,则查询名字中包含 name 的员工数据。其中 Employee::getName 表示使用 Employee 实体类的 getName 方法进行筛选操作。
  6. queryWrapper.orderByDesc(Employee::getUpdateTime) 给 queryWrapper 添加排序条件,按照员工更新时间降序排列。其中 Employee::getUpdateTime 表示使用 Employee 实体类的 getUpdateTime 方法进行排序操作。
  7. employeeService.page(pageInfo,queryWrapper) 执行查询,将查询结果存放在 pageInfo 对象中,最终返回 pageInfo。
  8. 最后,接口返回一个 R 类对象,使用 success 方法将 pageInfo 封装到响应体中。R 是一个通用的响应类,封装了接口响应的数据和状态等信息。

3.4 功能测试

代码编写完毕之后,我们需要将工程重启, 完毕之后直接访问管理系统首页, 默认就会打开员工管理的列表页面, 我们可以查看列表数据是否可以正常展示, 也可以通过分页插件来测试分页功能, 及员工姓名的模糊查询功能。

在进行测试时,可以使用浏览器的监控工具查看页面和服务端的数据交互细节。 并借助于debug的形式, 根据服务端参数接收及逻辑执行情况。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kgoO7taS-1688460908094)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202304021805171.png)]

测试过程中可以发现,对于员工状态字段(status)服务端返回的是状态码(1或者0),但是页面上显示的则是“正常”或者“已禁用”,这是因为页面中在展示数据时进行了处理。

image-20210730010606005

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RQTKX90k-1688460908095)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131541053.png)]

F8 两次

image-20230606091116014

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XIHfXtYv-1688460908095)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131541021.png)]

image-20230606091238699

3次F8

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6kithCUS-1688460908096)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131541653.png)]

F8

==>  Preparing: SELECT COUNT(*) FROM employee
==> Parameters: 
<==    Columns: COUNT(*)
<==        Row: 4
<==      Total: 1
==>  Preparing: SELECT id,username,name,password,phone,sex,id_number,status,create_time,update_time,create_user,update_user FROM employee ORDER BY update_time DESC LIMIT ?
==> Parameters: 10(Long)
<==    Columns: id, username, name, password, phone, sex, id_number, status, create_time, update_time, create_user, update_user
<==        Row: 1665721390604546050, lisi, 李四, e10adc3949ba59abbe56e057f20f883e, 13528659545, 1, 358956221565220, 1, 2023-06-05 22:05:02, 2023-06-05 22:05:02, 1, 1
<==        Row: 1665562193044803586, zhangsan, zs, e10adc3949ba59abbe56e057f20f883e, 13412345678, 1, 350689566956852556, 1, 2023-06-05 11:32:26, 2023-06-05 11:32:26, 1, 1
<==        Row: 1665561007872905218, zhagsan, 张三, e10adc3949ba59abbe56e057f20f883e, 13412345678, 1, 350689566956852556, 1, 2023-06-05 11:27:44, 2023-06-05 11:27:44, 1, 1
<==        Row: 1, admin, 管理员, e10adc3949ba59abbe56e057f20f883e, 13812312312, 1, 110101199001010047, 1, 2021-05-06 17:20:07, 2021-05-10 02:24:09, 1, 1
<==      Total: 4

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hbaplbux-1688460908097)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131541028.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GTIWVIAb-1688460908097)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131541445.png)]

image-20230606091640224

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dsma9Jn4-1688460908098)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131542324.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3aifa4Pi-1688460908099)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131542565.png)]

image-20230606092639214

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pB8Y46cJ-1688460908100)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131542584.png)]

image-20230606092923413

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9PS8Gm5r-1688460908100)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131542621.png)]

4. 启用/禁用员工账号

4.1 需求分析

在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。如果某个员工账号状态为正常,则按钮显示为 “禁用”,如果员工账号状态为已禁用,则按钮显示为"启用"。

需要注意,只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示。

A. admin 管理员登录

image-20210730010858705

B. 普通用户登录

image-20210730010941399

4.2 程序执行流程

4.2.1 页面按钮动态展示

在上述的需求中,我们提到需要实现的效果是 : 只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示 , 页面中是怎么做到只有管理员admin能够看到启用、禁用按钮的?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QAWJruTQ-1688460908101)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131543430.png)]

1). 在列表页面(list.html)加载时, 触发钩子函数created, 在钩子函数中, 会从localStorage中获取到用户登录信息, 然后获取到用户名

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wo8yecOq-1688460908101)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131543117.png)]

取到之后 给模型数据 user

image-20210730012044171

拿到模型数据 user 之后 通过Vue指令v-if进行判断,如果登录用户为admin将展示 启用/禁用 按钮, 否则不展示

         </el-button>
            <el-button
              type="text"
              size="small"
              class="delBut non"
              @click="statusHandle(scope.row)"
              v-if="user === 'admin'" // 通过Vue指令v-if进行判断,如果登录用户为admin将展示 启用/禁用 按钮, 否则不展示
            >
              {{ scope.row.status == '1' ? '禁用' : '启用' }}
            </el-button>

image-20230606162420563

2). 在页面中, 通过Vue指令v-if进行判断,如果登录用户为admin将展示 启用/禁用 按钮, 否则不展示

image-20210730012256779
4.2.2 执行流程分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6RBMtaOu-1688460908102)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131543885.png)]

1、页面发送ajax请求,将参数(id、status)提交到服务端

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SLdGYm2y-1688460908102)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131543443.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TASciZ0C-1688460908103)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131543717.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rdDZUSZ8-1688460908103)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131544148.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wkob9QrV-1688460908103)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131544276.png)]

2 、服务端Controller接收页面提交的数据并调用Service更新数据

3、Service调用Mapper操作数据库

为什么点击禁用或启用按钮 会发出请求

1). 当管理员admin点击 “启用” 或 “禁用” 按钮时, 调用方法statusHandle

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mUTubERS-1688460908104)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131544669.png)]

image-20210730012723560

scope.row : 获取到的是这一行的数据信息 ;

同时会传出scope.row过来 scope.row就是我们看到的数据 全封装的JSON对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pt04BjlQ-1688460908104)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131544852.png)]

2). statusHandle方法中进行二次确认, 然后发起ajax请求, 传递id、status参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QBSqRSyF-1688460908105)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131544876.png)]

 //状态修改
          statusHandle (row) {
            this.id = row.id    //获得id的值
            this.status = row.status    //获得状态的值
            this.$confirm('确认调整该账号的状态?', '提示', {
              'confirmButtonText': '确定',
              'cancelButtonText': '取消',
              'type': 'warning'
              }).then(() => {
              enableOrDisableEmployee({ 'id': this.id, 'status': !this.status ? 1 : 0 }).then(res => {
                console.log('enableOrDisableEmployee',res)
                if (String(res.code) === '1') {
                  this.$message.success('账号状态更改成功!')
                  this.handleQuery()
                }
              }).catch(err => {
                this.$message.error('请求出错了:' + err)
              })
            })
          },
image-20210730013011861

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N3jZsQaM-1688460908105)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131545498.png)]

image-20210730013210948
// 修改---启用禁用接口
function enableOrDisableEmployee (params) {
  return $axios({
    url: '/employee',
    method: 'put',
    data: { ...params }
  })
}

最终发起异步请求, 请求服务端, 请求信息如下:

请求说明
请求方式PUT
请求路径/employee
请求参数{“id”:xxx,“status”:xxx}

{…params} : 三点是ES6中出现的扩展运算符。作用是遍历当前使用的对象能够访问到的所有属性,并将属性放入当前对象中。

4.3 代码实现

在开发代码之前,需要梳理一下整个程序的执行过程:

1). 页面发送ajax请求,将参数(id、status)提交到服务端

2). 服务端Controller接收页面提交的数据并调用Service更新数据

3). Service调用Mapper操作数据库

    /**
     * 根据id修改员工信息
     * @param employee
     * @return
     */
    @PutMapping
    public R<String> update(@RequestBody Employee employee){
        
        log.info(employee.toString());

        return null;
    }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FNxCuOsq-1688460908106)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131545374.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZHK4wg1i-1688460908106)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131550246.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tiOF9DeG-1688460908107)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131550257.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oms8Xy8O-1688460908107)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131550686.png)]

启用、禁用员工账号,本质上就是一个更新操作,也就是对status状态字段进行操作。在Controller中创建update方法,此方法是一个通用的修改员工信息的方法。

/**
 * 根据id修改员工信息
 * @param employee
 * @return
 */
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
    log.info(employee.toString());

    Long empId = (Long)request.getSession().getAttribute("employee");

    employee.setUpdateTime(LocalDateTime.now());
    employee.setUpdateUser(empId);
    employeeService.updateById(employee);

    return R.success("员工信息修改成功");
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BYsdqUYa-1688460908108)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131550918.png)]

代码解释1

以上代码是一个Java Spring Boot框架中的控制器方法。代码使用了@PutMapping注解,表示该方法处理HTTP的PUT请求。

方法的目的是根据提供的员工ID(通过request.getSession().getAttribute("employee")获取)来修改员工信息。方法的参数包括HttpServletRequest对象和Employee对象,其中HttpServletRequest对象用于获取当前会话中的员工ID,Employee对象包含了需要更新的员工信息。

代码的执行步骤如下:

  1. 首先,日志记录器log会打印出employee对象的字符串表示,以便在日志中查看传入的员工信息。
  2. 通过request.getSession().getAttribute("employee")获取当前会话中的员工ID,并将其赋值给empId变量。
  3. 使用LocalDateTime.now()获取当前的时间,并将其设置为员工信息的更新时间。
  4. empId设置为员工信息的更新用户。
  5. 调用employeeService.updateById(employee)方法来更新数据库中对应员工ID的员工信息。
  6. 最后,返回一个成功的R对象,包含了一个消息字符串"员工信息修改成功"。

需要注意的是,代码中的R<String>表示返回一个包含泛型为StringR对象。R可能是自定义的响应对象,其中包含了一些用于表示请求结果的字段,例如状态码、消息等。具体实现可能需要查看项目中的相关代码。

在详细一点

以上代码是一个Java Spring Boot框架中的控制器方法。代码使用了@PutMapping注解,表示该方法处理HTTP的PUT请求。

方法的目的是根据提供的员工ID(通过request.getSession().getAttribute("employee")获取)来修改员工信息。方法的参数包括HttpServletRequest对象和Employee对象,其中HttpServletRequest对象用于获取当前会话中的员工ID,Employee对象包含了需要更新的员工信息。

代码的执行步骤如下:

  1. 首先,日志记录器log会打印出employee对象的字符串表示,以便在日志中查看传入的员工信息。
  2. 通过request.getSession().getAttribute("employee")获取当前会话中的员工ID,并将其赋值给empId变量。
  3. 使用LocalDateTime.now()获取当前的时间,并将其设置为员工信息的更新时间。
  4. empId设置为员工信息的更新用户。
  5. 调用employeeService.updateById(employee)方法来更新数据库中对应员工ID的员工信息。
  6. 最后,返回一个成功的R对象,包含了一个消息字符串"员工信息修改成功"。

需要注意的是,代码中的R<String>表示返回一个包含泛型为StringR对象。R可能是自定义的响应对象,其中包含了一些用于表示请求结果的字段,例如状态码、消息等。具体实现可能需要查看项目中的相关代码。

不明白的点

方法的参数有两个:HttpServletRequest对象和Employee对象。HttpServletRequest对象用于获取当前会话中的员工ID,Employee对象包含了需要更新的员工信息。

非常抱歉,我解释中的那部分可能不够清晰。我将重新解释方法的参数以及它们的作用。

  1. HttpServletRequest对象:HttpServletRequest是Java Servlet中的一个类,它表示客户端发出的HTTP请求。在这个方法中,我们将HttpServletRequest对象作为参数传递进来,以便获取当前会话中的员工ID。

    当前会话是指与客户端(通常是Web浏览器)的交互过程中,服务器保持的关于该客户端的信息。会话可以跨多个HTTP请求,并且在整个会话期间保持活动状态。通过HttpServletRequest对象,我们可以获取与会话相关的信息,例如会话中存储的属性值。

    在这段代码中,我们使用request.getSession().getAttribute("employee")来获取会话中名为"employee"的属性值。这个属性值应该是当前登录员工的ID。

  2. Employee对象:Employee是一个自定义的类,它表示员工的信息。在这个方法中,我们将Employee对象作为参数传递进来,其中包含了需要更新的员工信息。

    Employee对象通常具有多个属性,例如姓名、职位、薪水等。在这段代码中,我们传入的Employee对象应该包含了需要进行更新的员工信息。通过这个对象,我们可以获取和设置员工的各种属性值。

    在方法的后续代码中,我们将使用Employee对象的方法来设置员工的更新时间和更新用户,然后将其传递给一个名为updateById的方法,以便在数据库中更新员工的信息。

总结:HttpServletRequest对象用于获取当前会话中的员工ID,而Employee对象包含了需要更新的员工信息。通过这两个参数,我们可以获取员工ID和更新的员工信息,并进行相应的操作,例如记录更新时间和更新用户,然后进行数据库的更新操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JgNNzjoe-1688460908108)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131550358.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RLM1JVjA-1688460908109)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131550262.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DXCcbbtw-1688460908109)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131551383.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sL4IpM3U-1688460908110)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131551952.png)]

5次F8

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uci1NjFu-1688460908110)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131551679.png)]

id不正确 更新失败

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5wKFdFBK-1688460908110)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131552765.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YldLBLMm-1688460908111)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131552126.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q1Dk3KPi-1688460908111)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131552710.png)]

正确id

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MUp6sEQQ-1688460908112)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131552901.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A0aS3BKK-1688460908112)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131553078.png)]

4.4 功能测试

代码编写完毕之后,我们需要将工程重启。 然后访问前端页面, 进行 “启用” 或 “禁用” 的测试。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ak6gN6U3-1688460908113)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202304021806965.png)]

测试过程中没有报错,但是功能并没有实现,查看数据库中的数据也没有变化。但是从控制台输出的日志, 可以看出确实没有更新成功。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xe7AtMoR-1688460908113)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202304021806475.png)]

而在我们的数据库表结构中, 并不存在该ID, 数据库中 风清扬 对应的ID为 1420038345634918401

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GTPRcVIU-1688460908113)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202304021806756.png)]

4.5 代码修复

4.5.1 原因分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GcJSgHbQ-1688460908114)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202304021806412.png)]

通过观察控制台输出的SQL发现页面传递过来的员工id的值和数据库中的id值不一致,这是怎么回事呢?

在分页查询时,服务端会将返回的R对象进行json序列化,转换为json格式的数据,而员工的ID是一个Long类型的数据,而且是一个长度为 19 位的长整型数据, 该数据返回给前端是没有问题的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ptku2prd-1688460908114)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202304021806699.png)]

那么具体的问题出现在哪儿呢?

问题实际上, 就出现在前端JS中, js在对长度较长的长整型数据进行处理时, 会损失精度, 从而导致提交的id和数据库中的id不一致。 这里,我们也可以做一个简单的测试,代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script>
        alert(1420038345634918401);
    </script>
</head>
<body>
</body>
</html>
4.5.2 解决方案

要想解决这个问题,也很简单,我们只需要让js处理的ID数据类型为字符串类型即可, 这样就不会损失精度了。同样, 大家也可以做一个测试:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script>
        alert("1420038345634918401");
    </script>
</head>
<body>
</body>
</html>

那么在我们的业务中, 我们只需要让分页查询返回的json格式数据库中, long类型的属性, 不直接转换为数字类型, 转换为字符串类型就可以解决这个问题了 , 最终返回的结果为 :

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YZfFYR5I-1688460908114)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202304021807347.png)]

4.5.3 代码修复

由于在SpringMVC中, 将Controller方法返回值转换为json对象, 是通过jackson来实现的, 涉及到SpringMVC中的一个消息转换器MappingJackson2HttpMessageConverter, 所以我们要解决这个问题, 就需要对该消息转换器的功能进行拓展。

具体实现步骤:

1). 提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换(资料中已经提供,直接复制到项目中使用)

2). 在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换

1). 引入JacksonObjectMapper

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 */
public class JacksonObjectMapper extends ObjectMapper {
    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

                .addSerializer(BigInteger.class, ToStringSerializer.instance)
                .addSerializer(Long.class, ToStringSerializer.instance)

                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

该自定义的对象转换器, 主要指定了, 在进行json数据序列化及反序列化时, LocalDateTime、LocalDate、LocalTime的处理方式, 以及BigInteger及Long类型数据,直接转换为字符串。

2). 在WebMvcConfig中重写方法extendMessageConverters

/**
 * 扩展mvc框架的消息转换器
 * @param converters
 */
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    log.info("扩展消息转换器...");
    //创建消息转换器对象
    MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
    //设置对象转换器,底层使用Jackson将Java对象转为json
    messageConverter.setObjectMapper(new JacksonObjectMapper());
    //将上面的消息转换器对象追加到mvc框架的转换器集合中
    converters.add(0,messageConverter);
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-teHTLONy-1688460908115)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131553415.png)]

测试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MQ5BLdhK-1688460908115)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131553663.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HDAo2cCe-1688460908116)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131553038.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l8pLvPDF-1688460908116)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131553871.png)]

4次 F8

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PQunt52v-1688460908119)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131554063.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4ycAP3ya-1688460908119)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131554799.png)]

放行

重新启动

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QPWiuaow-1688460908119)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131554310.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BN09ZbqH-1688460908120)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131554974.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mDQQgjAk-1688460908120)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131554126.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R0ztbrd4-1688460908121)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131554438.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m8x4w378-1688460908121)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131554302.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qYhpXRqz-1688460908122)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131555469.png)]

5. 编辑员工信息

在开发代码之前需要梳理一下操作过程和对应的程序的执行流程:
1、点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]
2、在add.html页面获取url中的参数[员工id]
3、发送ajax请求,请求服务端,同时提交员工id参数
4、服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
5、页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
6、点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端
7、服务端接收员工信息,并进行处理,完成后给页面响应
8、页面接收到服务端响应信息后进行相应处理

注意:add.html页面为公共页面,新增员工和编辑员工都是在此页面操作

1、点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-erMUGilk-1688460908122)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131555057.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sEcqqWv7-1688460908123)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306142117010.png)]

2、在add.html页面获取url中的参数[员工id]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eSfqYXtW-1688460908123)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131555202.png)]

3、发送ajax请求,请求服务端,同时提交员工id参数
4、服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面

5、页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显

我们刚才看到的报的404 把这个id作为请求路径的一部分

为什么要发送请求

因为我们的服务端接收到请求之后 可以根据id去查询用户 然后回显到页面上 以JSON的形式响应给页面

页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KztVmtZU-1688460908124)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131555540.png)]

注意点:

在修改的过程中有两次与服务端进行交互

第一次 发送请求 查询相对应的数据进行回显

第二次 当我们点击保存按钮的时候 把我们修改之后的数据 提交到我们的服务端

5.1 需求分析

在员工管理列表页面点击 “编辑” 按钮,跳转到编辑页面,在编辑页面回显员工信息并进行修改,最后点击 “保存” 按钮完成编辑操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pwxDABQA-1688460908124)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202304021807123.png)]

那么从上述的分析中,我们可以看出当前实现的编辑功能,我们需要实现两个方法:

A. 根据ID查询, 用于页面数据回显

B. 保存修改

执行流程(视频版本)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8b9eh6MQ-1688460908125)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131555348.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XNx1qGS7-1688460908125)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131555106.png)]

5.2 程序执行流程

在开发代码之前需要梳理一下操作过程和对应的程序的执行流程:

1). 点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ov7g9NOn-1688460908126)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202304021807323.png)]

2). 在add.html页面获取url中的参数[员工id]

3). 发送ajax请求,请求服务端,同时提交员工id参数

4). 服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0BULTI6z-1688460908126)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202304021807595.png)]

5). 页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显

6). 点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端

7). 服务端接收员工信息,并进行处理,完成后给页面响应

8). 页面接收到服务端响应信息后进行相应处理

image-20210730230533123

注意:add.html页面为公共页面,新增员工和编辑员工都是在此页面操作

5.3 代码实现

5.3.1 根据ID查询

经过上述的分析,我们看到,在根据ID查询员工信息时,请求信息如下:

请求说明
请求方式GET
请求路径/employee/{id}

代码实现:

在EmployeeController中增加方法, 根据ID查询员工信息。

/**
 * 根据id查询员工信息
 * @param id
 * @return
 */
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id){
    log.info("根据id查询员工信息...");
    Employee employee = employeeService.getById(id);
    if(employee != null){
        return R.success(employee);
    }
    return R.error("没有查询到对应员工信息");
}

代码解释

解释1

以上代码是一个基于Spring框架的Java代码片段,用于实现一个RESTful API接口。代码中包含了一个GET请求方法,用于根据员工ID查询员工信息。

解释如下:

  1. @GetMapping("/{id}"):这是一个注解,指定了HTTP GET请求的URL路径。/{id}表示该路径中需要包含一个名为id的路径参数。
  2. public R<Employee> getById(@PathVariable Long id):这是一个公共方法,返回类型为R。R是一个自定义的响应对象,用于封装API接口的返回结果。方法名为getById,接收一个名为id的路径参数。
  3. log.info("根据id查询员工信息..."):这是一个日志输出语句,用于在日志中记录一条信息,表示正在执行根据员工ID查询员工信息的操作。
  4. Employee employee = employeeService.getById(id):这是调用一个名为employeeService的服务对象的getById方法,传入id参数,用于根据员工ID查询员工信息。查询结果会保存在employee对象中。
  5. if(employee != null):如果查询到了员工信息(employee不为null),则执行下一行代码;否则,执行返回错误信息的逻辑。
  6. return R.success(employee):如果查询到了员工信息,将员工信息作为成功响应的数据,使用R.success方法封装响应结果,然后返回。
  7. return R.error("没有查询到对应员工信息"):如果没有查询到员工信息,将错误信息作为错误响应的数据,使用R.error方法封装响应结果,然后返回。

总体而言,这段代码实现了一个GET请求接口,通过传入员工ID来查询对应的员工信息,并将查询结果通过自定义的响应对象R进行封装后返回。

解释2

当客户端发送一个GET请求到指定的URL路径时(例如:/{id}),该方法会被执行。在这个URL路径中,我们期望包含一个名为id的路径参数,以便指定要查询的员工的ID。

在方法体内部,首先通过日志输出记录正在执行的操作,即根据员工ID查询员工信息。

然后,通过调用一个名为employeeService的服务对象的getById方法,将传入的id参数作为查询条件,来获取对应的员工信息。查询结果将保存在名为employee的对象中。

接下来,通过判断employee对象是否为null来判断是否成功查询到员工信息。如果employee不为null,表示查询成功,将员工信息作为成功响应的数据。代码使用R.success方法将响应结果封装为一个成功响应对象R,并将其返回给客户端。

如果employee为null,表示没有查询到对应的员工信息,代码使用R.error方法将错误信息封装为一个错误响应对象R,并将其返回给客户端。

简而言之,该代码片段实现了一个GET请求接口,客户端通过传递员工ID来查询对应的员工信息。如果查询成功,返回包含员工信息的成功响应;如果查询失败,返回包含错误信息的错误响应。

详细解释 Employee employee = employeeService.getById(id); 这点不是很明白 能不能在详细解释一下

当代码执行到Employee employee = employeeService.getById(id);这一行时,它调用了名为employeeService的服务对象的getById方法,并传递了id参数。

在代码中,employeeService是一个封装了员工相关操作的服务对象。通过调用getById方法,它会根据传入的id参数查询对应的员工信息。

这里的getById方法可能会包含数据库查询操作或者其他方式的数据获取逻辑,具体实现取决于employeeService对象的具体实现。它会根据传入的id参数查询数据库或其他数据源,以获取对应id的员工信息。

查询结果将会被存储在名为employeeEmployee对象中。这个对象将包含查询到的员工信息,例如姓名、职位、工资等。

最后,employee对象将用于后续的判断和处理。根据查询结果,代码会决定返回成功响应(包含employee对象作为响应数据)或者返回错误响应(表示没有查询到对应的员工信息)。

总而言之,Employee employee = employeeService.getById(id);这一行代码的作用是调用服务对象的方法来获取对应id的员工信息,并将查询结果存储在名为employee的对象中,以供后续处理和返回响应使用。

employeeService是一个处理员工信息的服务对象。getById方法是该服务对象中的一个功能,它的作用是根据传入的员工ID来获取相应的员工信息。

在代码中,通过employeeService.getById(id)这一行,程序会调用getById方法,并传入id作为参数。这样,getById方法就知道要查询哪个员工的信息。

getById方法会根据提供的id,在数据库或其他数据源中查找相应的员工信息。查询的结果将会被赋值给一个叫employee的变量,它是一个Employee类型的对象。

最后,根据查询结果的情况,代码会决定返回成功响应或错误响应。如果成功找到了员工信息,就会将employee作为成功响应的数据返回。如果没有找到对应的员工信息,就会返回一个错误提示。

简单来说,这段代码的目的是根据传入的员工ID,通过调用employeeServicegetById方法来获取对应员工的信息,并根据查询结果返回相应的响应。

Employee employee = employeeService.getById(id); 表示获取员工信息的对象实例。

employeeService 是一个依赖注入的 Bean,它实现了 EmployeeService 接口,该接口定义了 getById 方法,方法的参数为 id,返回值为 Employee 对象。

getById 方法的实现是通过调用数据库中存储的员工信息的 id 字段来获取员工信息的对象实例。如果数据库中的员工信息与传入的 id 值匹配,则返回员工信息的对象实例,否则返回 null

由于此接口定义为 RESTful API,因此返回的员工信息对象实例应该遵循 HTTP 响应状态码和响应头规范。通常情况下,响应状态码为 200(成功) 或 404(未找到资源)。

在以上代码中,如果员工信息不为空,则返回响应对象 R.success(employee),表示员工信息已成功查询到;否则,返回错误响应 R.error("没有查询到对应员工信息"),表示无法查询到员工信息。

测试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4LVFSx7e-1688460908127)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131556298.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pbVuRtMA-1688460908127)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131557745.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yhWf808k-1688460908127)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131557489.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ULIpWPwr-1688460908128)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131557465.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vyjNO7Bc-1688460908128)(https://cdn.jsdelivr.net/gh/zyziga/picodemo/takeaway/day02/202306131557210.png)]

5.3.2 修改员工

经过上述的分析,我们看到,在修改员工信息时,请求信息如下:

请求说明
请求方式PUT
请求路径/employee
请求参数{…} json格式数据

代码实现:

在EmployeeController中增加方法, 根据ID更新员工信息。

/**
 * 根据id修改员工信息
 * @param employee
 * @return
 */
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
    log.info(employee.toString());

    Long empId = (Long)request.getSession().getAttribute("employee");

    employee.setUpdateTime(LocalDateTime.now());
    employee.setUpdateUser(empId);
    employeeService.updateById(employee);

    return R.success("员工信息修改成功");
}

5.4 功能测试

代码编写完毕之后,我们需要将工程重启。 然后访问前端页面, 按照前面分析的操作流程进行测试,查看数据是否正常修改即可。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值