苍穹外卖项目

Day01

软件开发流程(面试题)

苍穹外卖项目_第1张图片

三种软件环境

苍穹外卖项目_第2张图片

项目中用到的技术

苍穹外卖项目_第3张图片 功能架构

苍穹外卖项目_第4张图片

 数据库表的设计

苍穹外卖项目_第5张图片

 yml配置

server:
  port: 8080
spring:
  application:
#    应用的名称  可选择配置
    name: reggie_take_out
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: root
mybatis-plus:
  configuration:
#    address_book ----->AddressBook
    #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: ASSIGN_ID

 设置静态资源映射

@Slf4j
@Configuration
public class WebMVCConfig extends WebMvcConfigurationSupport {
    /**
     * 设置静态资源映射
     * @param registry
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("开始进行静态资源映射...");
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");

    }

登录接口

苍穹外卖项目_第6张图片

  • @RequestBody 主要用于接收前端传递给后端的json字符串(请求体中的数据)
  • HttpServletRequest 作用:如果登录成功,将员工对应的id存到session一份,这样想获取一份登录用户的信息就可以随时获取出来
@RestController
@Slf4j
@RequestMapping("/employee")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    /**
     * 登录功能
     *
     * @param request
     * @param employee
     * @return
     * @RequestBody 主要用于接收前端传递给后端的json字符串(请求体中的数据)
     * HttpServletRequest 作用:如果登录成功,将员工对应的id存到session一份,这样想获取一份登录用户的信息就可以随时获取出来
     */
    @PostMapping("/login")
// wrapper.eq("实体类::查询字段", "条件值"); //相当于where条件
    public R login(HttpServletRequest request, @RequestBody Employee employee) {
        //1 将页面提交的密码进行md5加密
        String password = employee.getPassword();
        password = DigestUtils.md5DigestAsHex(password.getBytes());

        //2 根据页面提交的用户名username查询数据库
        QueryWrapper wrapper = new QueryWrapper<>();
        wrapper.eq("username", employee.getUsername());
        Employee emp = employeeService.getOne(wrapper);

        //3 如果没有查询到则返回登陆失败信息
        if (emp == null) {
            return R.error("登陆失败");
        }

        //4 密码比对 如果不一致返回登陆失败结果
        if (!emp.getPassword().equals(password)) {
            return R.error("登陆失败");
        }

        //5 查看员工状态  如果说已禁用状态 返回禁用结果
        if (emp.getStatus() == 0) {
            return R.error("账号已经禁用");
        }

        //6 登陆成功 将员工id存入Session并返回登陆成功结果
        request.getSession().setAttribute("employee", emp.getId());
        return R.success(emp);
    }

完善登录功能

问题分析:

  • 之前的登录功能,我们不登录,直接访问 http://localhost/backend/index.html 也可以正常访问,这显然是不合理的
  • 我们希望看到的效果是,只有登录成功才能看到页面,未登录状态则跳转到登录页面
  • 那么具体改如何实现呢?使用过滤器或拦截器,在过滤器或拦截器中判断用户是否登录,然后在选择是否跳转到对应页面。
  • 苍穹外卖项目_第7张图片

1  测试Filter拦截路径 

/**
 * 检查用户是否完成登陆的过滤器
 */
@Slf4j
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
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);
    }

2  并在启动类上加上 @servletComponentScan

@SpringBootApplication
@ServletComponentScan
public class ReggieApplication {
	public static void main(String[] args) {
		SpringApplication.run(ReggieApplication.class, args);
	}
}

 苍穹外卖项目_第8张图片

  

 上面我们已经能成功拦截到路径了,那现在我们来开发逻辑,主要分为以下几个步骤

/**
 * 检查用户是否完成登陆的过滤器
 */
@Slf4j
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
public class LoginCheckFilter implements Filter {

    /**
     *  路径匹配器
     * 判断本次请求是否需要处理
     * 使用Spring概念模型: PathMatcher
     */
    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();
        //定义不需要的路径请求
        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 判断用户登录状态,如果已经登录,则直接放行  当初存的 session是employee 所以拿到他来判断
        if (request.getSession().getAttribute("employee") != null) {
            filterChain.doFilter(request, response);
            return;
        }
        //5 如果未登录返回未登录结果  通过输出流 向客户端页面响应数据
        //未登录状态为什么要返回一个error呢?而且msg为NOTLOGIN
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;
    }

    /**
     * 路径匹配, (check)检查本次请求是否需要放行
     * @param requestURI
     * @param urls
     * @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;
    }
}

测试登录

当我们直接访问 http://localhost/backend/index.html 时,日志输出如下

: 用户未登录
: 用户id为:null

随后将自动跳转至登录页面

: 拦截到请求:/employee/login
: 本次请求:/employee/login,不需要处理

成功登录后

: 拦截到请求:/employee/page
: 用户已登录,id为1

那么至此,登录功能就已经做好了

退出登录

苍穹外卖项目_第9张图片

 登出功能的后端操作很简单,只要删除session就好了

/**
 * 员工退出
 * @param request
 * @return
 */
@PostMapping("/logout")
public R logout(HttpServletRequest request){
    // 清理Session中的用户id
    request.getSession().removeAttribute("employee");
    return R.success("退出成功");
}

Day02

新增员工

流程分析

实现功能之前,我们先梳理一下整个执行流程

  1. 页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
  2. 服务端Controller接收页面提交的数据并调用Service将数据进行保存
  3. Service调用Mapper操作数据库,保存数据

我们主要做第二步和第三步
先测试一下是否能成功接收到员工信息,用日志输出看一下

JAVA

1
2
3
4
5
@PostMapping
public Result save(@RequestBody Employee employee){
    log.info("新增的员工信息:{}",employee.toString());
    return null;
}

启动服务器,测试添加员工功能,随便输点数据,查看日志

苍穹外卖项目_第10张图片

: 新增的员工信息:Employee(id=null, username=kyle, name=KKK, password=null, phone=15811234568, sex=1, idNumber=111111222222333333, status=null, createTime=null, updateTime=null, createUser=null, updateUser=null)

但此时的表单中只有部分数据,id,password,status,createTime等都还没有指定。
那么我们现在来逐一分析这几项该如何设置

  • id 这个就用自动生成的就好了(雪花算法/自动递增)
  • password 当你注册某些教育网站的时候,一般都会给你默认指定一个密码(身份证后六位,123456等),所以我们这里的解决策略就直接指定一个123456了,但是这个密码不能直接在数据库中设为默认值,因为数据库设置的默认值无法加密
  • status 设定员工的状态,1表示启用,0表示禁用,这个就可以直接用默认值了,不需要加密,默认给个1即可
  • createTime 创建时间,这个就指定当前时间就好了
  • updateTime 作用同上
  • createUser 这个是创建人的ID,我们首先需要一个管理员账号登录到后台管理界面,然后才能添加员工信息,所以我们也需要对这个员工信息的创建人,进行设置,避免出现莫名的员工账号,依靠这个可以溯源
  • updateUser 作用同上

综上所述,我们只需要设置密码,创建时间和更新时间,创建人ID和修改人ID
从前端代码来看,我们需要发送Post请求,且不需要参数

/**
 * 新增员工
 *
 * @param employee
 * @return
 */
@PostMapping
public R 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("添加员工成功");
}

那么至此添加员工的功能就开发完毕了,启动服务器,测试一下添加员工,添加完毕后,如果没有问题,会显示添加员工成功,之后去数据库查看,数据库中也有对应的数据,且密码也经过了加密,createTime和createUser等数据也都有。

值得注意的一点是,username不能重复,因为在建表的时候设定了unique,只能存在唯一的username,如果存入相同的username则会报错
控制台报错
java.sql.SQLIntegrityConstraintViolationException: Duplicate entry ‘zhangsan' for key 'employee.idx_username'

 苍穹外卖项目_第11张图片

 但是这个报错目前也不太人性化,咱也不知道具体为啥添加失败了,所以我们还得继续完善一下,那么具体该怎么完善呢?我们在之前使用过统一异常处理。

完善全局异常处理器,并测试

com.itheima.common包下创建一个全局异常处理类GlobalExceptionHandler,并添加exceptionHandler方法用来捕获异常,并返回结果

/**
 * 全局异常处理类
 * 底层基于代理 aop
 */
@ControllerAdvice(annotations = {RestController.class, Controller.class}) //通知
@ResponseBody // 封装成为json数据来返回
@Slf4j
public class GlobalExceptionHandler  {

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

    }
}

先用日志输出一下看看能不能正常运行,这也是代码开发的一个好习惯
启动服务器,新增员工测试,输入数据库中已存在的username,这次会报错未知错误(如果你还没报未知错误,建议先调试好再往下进行)
控制台日志输出的错误信息为Duplicate entry 'zhangsan' for key 'employee.idx_username'

然后我们再来开发具体的异常处理逻辑
我们希望给出的错误信息为该用户名已存在,所以我们就需要对错误信息来进行判断,如果错误信息中包含Duplicate entry,则说明有条目是重复的,在本案例中,只可能是username重复了,所以我们在用split()方法来对错误信息切片,取出重复的username,采用字符串拼接的方式,告知该用户已经存在了

/**
 * 全局异常处理类
 * 底层基于代理 aop
 */
@ControllerAdvice(annotations = {RestController.class, Controller.class}) //通知
@ResponseBody // 封装成为json数据来返回
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 异常处理方法
     *
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R exceptionHandler(SQLIntegrityConstraintViolationException exception) {
        log.error(exception.getMessage());
        //如果包含Duplicate entry 说明有条目重复
        if (exception.getMessage().contains("Duplicate entry")) {
            //对字符串进行切片
            String[] split = exception.getMessage().split(" ");
            String username = split[2];
            //拼串作为错误信息 返回
            return R.error("用户名:" + username + "已存在");
        }
        //如果是别的错误也没办法
        return R.error("未知错误");
    }

接下来重启服务器,测试添加功能,输入已经存在的username,输出的错误信息符合我们的预期苍穹外卖项目_第12张图片员工信息分页查询 

苍穹外卖项目_第13张图片

 配置MyBatisPlus分页插件

新建com.itheima包,并在其中新建MPConfig

@Configuration
public class MPConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }

我们先来访问页面,打开开发者工具,点击员工管理,监测一下Network请求,会看到这么个东西

请求网址: http://localhost/employee/page?page=1&pageSize=10
请求方法: GET

使用GET发送的请求,请求参数在URL中

苍穹外卖项目_第14张图片

在搜索框中输入123,进行查询,发现name也出现在URL中了

请求网址: http://localhost/employee/page?page=1&pageSize=10&name=123
请求方法: GET

编写具体的业务逻辑

我们先来用日志输出一下,看看能不能正常接收到数据

JAVA

1
2
3
4
5
@GetMapping("/page")
    public R page(int page, int pageSize, String name) {
        log.info("page={},pageSize={},name={}",page,pageSize,name);
        return null;
    }

 重新启动服务器,在搜索框输入123并搜索,查看日志输出,符合我们的预期

: page=1,pageSize=10,name=123

 一切正常后,完善业务逻辑
 @GetMapping("/page")
    public R page(int page, int pageSize, String name) {
        log.info("page={},pageSize={},name={}", page, pageSize, name);
        //构造分页构造器
        Page pageInfo = new Page(page, pageSize);
        //构造条件构造器
        LambdaQueryWrapper wrapper = new LambdaQueryWrapper();
        //添加过滤条件  意思就是name!=null 才会添加
        wrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);
        //对查询的结果进行 降序 排列 根据时间进行更新
        wrapper.orderByDesc(Employee::getUpdateTime);
        //执行查询
        employeeService.page(pageInfo, wrapper);

        return R.success(pageInfo);
    }

苍穹外卖项目_第15张图片

补充说明

  • 为什么后端传给页面的status数据为Integer类型,到页面展示效果的时候显示的是已禁用或者正常?
    • 看一下源码就知道了
      三目运算符+插值表达式

      HTML

      1
      2
      3
      4
      
      
      
          
      

Day03

启用/禁用员工账号

需求分析

  1. 在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。
  2. 需要注意,只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示。
  3. 管理员admin登录系统可以对所有员工账号进行启用、禁用操作。
  4. 如果某个员工账号状态为正常,则按钮显示为“禁用”,如果员工账号状态为已禁用,则按钮显示为“启用”

苍穹外卖项目_第16张图片

动态按钮显示分析

怎么才能做到:只有当登录的是管理员账号时,才能看到启用/禁用按钮呢?

  • 当我们加载完页面的时候,获取一下当前登录账号的用户名,也就是username

    JS

    1
    2
    3
    4
    
    created() {
        this.init()
        this.user = JSON.parse(localStorage.getItem('userInfo')).username
    }
    
  • 随后判断一下这个用户名是不是admin,如果是的话就显示启用/禁用,否则不显示
    那么我们现在就来button里设置一下,使用v-if来判断

    HTML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    
        {{ scope.row.status == '1' ? '禁用' : '启用' }}
    
    

Ajax请求发送过程

  1. 页面发送ajax请求,将参数(id、status)提交到服务端
  2. 服务端Controller接收页面提交的数据并调用Service更新数据
  3. Service调用Mapper操作数据库
  • 前端代码分析

    • button
    • statusHandle
    • enableOrDisableEmployee

    从禁用/启用的按钮中,我们可以看到是绑定了一个名为statusHandle(scope.row)函数

    HTML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    
        {{ scope.row.status == '1' ? '禁用' : '启用' }}
    
    

  • 后端代码分析
    启用、禁用员工账号,本质上就是一个更新操作,也就是对status状态字段进行操作在Controller中创建update方法,此方法是一个通用的修改员工信息的方法
    只不过现在我们的update只需要修改status,而后面我们还有修改员工其他信息的业务,根据传进来的employee

    JAVA

    1
    2
    3
    4
    5
    
    @PutMapping
    public Result update(@RequestBody Employee employee) {
        log.info(employee.toString());
        return null;
    }
    

    按照惯例,我们先启动一下服务器,看看是否能接收到employee对象数据
    点击禁用按钮,日志输出如下

    : Employee(id=1575840690817011700, username=null, name=null, password=null, phone=null, sex=null, idNumber=null, status=0, createTime=null, updateTime=null, createUser=null, updateUser=null)

  • id和status均有值,符合我们的预期,那我们继续往下进行

  • 完善update方法的代码逻辑
    状态修改我们已经在前面完成了,这里来编写一下更新时间和更新用户
    依旧是通过我们之前存的session来获取当前user的id

    JAVA

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    @PutMapping
    public Result update(@RequestBody Employee employee, HttpServletRequest request) {
        log.info(employee.toString());
        Long id = (Long) request.getSession().getAttribute("employee");
        employee.setUpdateUser(id);
        employee.setUpdateTime(LocalDateTime.now());
        employeeService.updateById(employee);
        return Result.success("员工信息修改成功");
    }
    

    查看数据库,我们发现status并没有被修改
    通过查看日志,我们发现更新操作并没有完成,这是怎么回事呢?

    ==> Preparing: UPDATE employee SET status=?, update_time=?, update_user=? WHERE id=?
    ==> Parameters: 0(Integer), 2022-10-04T09:37:21.459(LocalDateTime), 1(Long), 1575840690817011700(Long)
    <== Updates: 0

  • 仔细观察这里的id值为1575840690817011700,而实际的id值为1575840690817011713
  • 问题的原因:
    • JS对Long型数据进行处理时丢失精度,导致提交的id和数据库中的id不一致。
  • 如何解决这个问题?
    • 我们可以在服务端给页面响应json数据时进行处理,将Long型数据统一转为String字符串

配置状态转换器

配置对象映射器JacksonObjectMapper,继承ObjectMapper
直接Copy这份代码也行

JAVA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import com.fasterxml.jackson.databind.DeserializationFeature;
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);
    }
}


扩展Mvc框架的消息转换器

JAVA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
@Slf4j
public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
    }

    @Override
    protected void extendMessageConverters(List> converters) {
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        //设置对象转化器,底层使用jackson将java对象转为json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        //将上面的消息转换器对象追加到mvc框架的转换器集合当中(index设置为0,表示设置在第一个位置,避免被其它转换器接收,从而达不到想要的功能)
        converters.add(0, messageConverter);
    }
}

再次测试

启动服务器,尝试禁用按钮
数据库中的status字段数据发生了改变,且页面上也显示已禁用,再次点击启用,也能正常操作

苍穹外卖项目_第17张图片

编辑员工信息

流程分析

在开发代码之前,我们先来梳理一下整个操作流程与对应程序的执行顺序

  1. 点击编辑按钮时,页面将跳转到add.html,并在url中携带参数员工id
  2. add.html页面中获取url中的参数员工id
  3. 发送ajax请求,请求服务端,同时提交员工id参数
  4. 服务端接受请求,并根据员工id查询员工信息,并将员工信息以json形式响应给页面
  5. 页面接收服务端响应的json数据,并通过Vue的双向绑定进行员工信息回显
  6. 点击保存按钮,发送ajax请求,将页面中的员工信息以json形式提交给服务端
  7. 服务端接受员工信息,并进行处理,完成后给页面响应
  8. 页面接收到服务端响应信息后进行相应处理

苍穹外卖项目_第18张图片

具体实现

  1. 点击编辑按钮时,页面将跳转到add.html,并在url中携带参数员工id
    编辑按钮绑定的点击事件为addMemberHandle(scope.row.id)

    HTML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    
        编辑
    
    
  2. add.html页面中获取url中的参数员工id

    JS

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    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)
        }
    }
    
  3. 发送ajax请求,请求服务端,同时提交员工id参数

    • 钩子函数

    add.html加载完毕之后,调用钩子函数,主要看其中requestUrlParam函数

    JS

    1
    2
    3
    4
    5
    6
    7
    
    created() {
        this.id = requestUrlParam('id')
        this.actionType = this.id ? 'edit' : 'add'
        if (this.id) {
        this.init()
        }
    }
    

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

    • getById
    • 健壮性更强

    JAVA

    1
    2
    3
    4
    5
    6
    
    @GetMapping("/{id}")
    public Result getById(@PathVariable Long id){
        log.info("根据id查询员工信息..");
        Employee employee = employeeService.getById(id);
        return Result.success(employee);
    }
    
  5. 页面接收服务端响应的json数据,并通过Vue的双向绑定进行员工信息回显
    created钩子函数中还调用了init函数
    该函数接收到服务端响应的json数据之后,先判断一下状态码,如果是1,则说明是操作成功
    随后将获取到的数据赋给表单,从而达到回显数据的效果

    JS

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    async init () {
        queryEmployeeById(this.id).then(res => {
            console.log(res)
            if (String(res.code) === '1') {
            console.log(res.data)
            this.ruleForm = res.data
            this.ruleForm.sex = res.data.sex === '0' ? '女' : '男'
            // this.ruleForm.password = ''
            } else {
            this.$message.error(res.msg || '操作失败')
            }
        })
    }
    
  6. 点击保存按钮,发送ajax请求,将页面中的员工信息以json形式提交给服务端
    • 保存按钮
    • submitForm
    • editEmployee

    HTML

    1
    2
    3
    4
    5
    6
    
    
        保存
    
    
  7. 服务端接受员工信息,并进行处理,完成后给页面响应
    由于修改员工信息也是发送的PUT请求,与之前启用/禁用员工账号是一致的,而且前面我们已经写过了PUT请求的Controller层
    所以当我们点击保存按钮时,调用submitForm函数,而在submitForm函数中我们又调用了editEmployee函数,发送PUT请求,实现修改功能

    JAVA

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    @PutMapping
    public Result update(@RequestBody Employee employee, HttpServletRequest request) {
        log.info(employee.toString());
        Long id = (Long) request.getSession().getAttribute("employee");
        employee.setUpdateUser(id);
        employee.setUpdateTime(LocalDateTime.now());
        employeeService.updateById(employee);
        return Result.success("员工信息修改成功");
    }
    
  8. 页面接收到服务端响应信息后进行相应处理
    员工信息修改成功之后,调用了goBack函数,跳转至员工管理页面

    JS

    1
    2
    3
    4
    5
    6
    7
    
    goBack(){
        window.parent.menuHandle({
            id: '2',
            url: '/backend/page/member/list.html',
            name: '员工管理'
        },false)
    }
    

那么至此,编辑员工信息的功能就完成了

公共字段自动填充

问题分析

  • 前面我们已经完成了对员工数据的添加与修改,在添加/修改员工数据的时候,都需要指定一下创建人、创建时间、修改人、修改时间等字段,而这些字段又属于公共字段,不仅员工表有这些字段,在菜品表、分类表等其他表中,也拥有这些字段。
  • 那我们有没有办法让这些字段在一个地方统一管理呢?这样可以简化我们的开发
    • 答案就是使用MybatisPlus给我们提供的公共字段自动填充功能

代码实现

  • 实现步骤
    1. 在实体类的属性上方加入@TableFiled注解,指定自动填充的策略
      • 修改Employee实体类
      • FieldFill

      JAVA

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      
      @Data
      public class Employee implements Serializable {
      
          private static final long serialVersionUID = 1L;
      
          private Long id;
      
          private String username;
      
          private String name;
      
          private String password;
      
          private String phone;
      
          private String sex;
      
          private String idNumber;//身份证号码
      
          private Integer status;
      
          @TableField(fill = FieldFill.INSERT)
          private LocalDateTime createTime;
      
          @TableField(fill = FieldFill.INSERT_UPDATE)
          private LocalDateTime updateTime;
      
          @TableField(fill = FieldFill.INSERT)
          private Long createUser;
      
          @TableField(fill = FieldFill.INSERT_UPDATE)
          private Long updateUser;
      }
      
    2. 按照框架要求编写元数据对象处理器,在此类中统一对公共字段赋值,此类需要实现MetaObjectHandler接口
      实现接口之后,重写两个方法,一个是插入时填充,一个是修改时填充
      关于字段填充方式,使用metaObject的setValue来实现
      关于id的获取,我们之前是存到session里的,但在MyMetaObjectHandler类中不能获得HttpSession对象,所以我们需要用其他方式来获取登录用户Id

      JAVA

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
      @Component
      @Slf4j
      public class MyMetaObjectHandler implements MetaObjectHandler {
          @Override
          public void insertFill(MetaObject metaObject) {
              log.info("公共字段自动填充(insert)...");
              log.info(metaObject.toString());
              metaObject.setValue("createTime", LocalDateTime.now());
              metaObject.setValue("updateTime", LocalDateTime.now());
          }
      
          @Override
          public void updateFill(MetaObject metaObject) {
              log.info("公共字段自动填充(update)...");
              log.info(metaObject.toString());
              metaObject.setValue("updateTime", LocalDateTime.now());
          }
      }
      

功能完善

  • 现在存在一个问题,如何获取当前登录用户的id值

    • 我们可以使用ThreadLocal来解决这个问题
  • 在学习ThreadLocal之前,我们需要先确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:

    1. LocalCheekFilter中的doFilter方法
    2. EmployeeController中的update方法
    3. MyMetaObjectHandler中的updateFill方法

现在我们在这三个方法中添加日志输出测试

  • doFilter

    JAVA

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    
    @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();
        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"));
            //在这里获取一下线程id
            long id = Thread.currentThread().getId();
            log.info("doFilter的线程id为:{}", id);
            filterChain.doFilter(request, response);
            return;
        }
    
        //5.如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
        log.info("用户未登录");
        log.info("用户id{}", request.getSession().getAttribute("employee"));
        response.getWriter().write(JSON.toJSONString(Result.error("NOTLOGIN")));
    
    }
    
  • update

    JAVA

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    @PutMapping
    public Result update(@RequestBody Employee employee, HttpServletRequest request) {
        log.info(employee.toString());
        //获取线程id
        long id = Thread.currentThread().getId();
        log.info("update的线程id为:{}", id);
        employeeService.updateById(employee);
        return Result.success("员工信息修改成功");
    }
    
  • updateFill

    JAVA

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("公共字段自动填充(update)...");
        log.info(metaObject.toString());
        long id = Thread.currentThread().getId();
        log.info("updateFill的线程id为:{}", id);
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("updateUser", new Long(1));
        metaObject.setValue("createUser", new Long(1));
    }
    
    重新启动服务器,登录页面并编辑员工信息(什么都不需要动),随后点击保存,随后查看日志输出信息

    com.blog.filter.LoginCheckFilter : doFilter的线程id为:34
    com.blog.controller.EmployeeController : update的线程id为:34
    com.blog.common.MyMetaObjectHandler : updateFill的线程id为:34

发现这三者确实是在同一个线程中

那么什么是ThreadLocal?

  • ThreadLocal并不是一个Thread,而是Thread的局部变量
  • 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本
  • 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本
  • ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。

ThreadLocal常用方法:

  • public void set(T value) 设置当前线程的线程局部变量的值
  • public T get() 返回当前线程所对应的线程局部变量的值

那么我们如何用ThreadLocal来解决我们上述的问题呢?

  • 我们可以在LoginCheckFilterdoFilter方法中获取当前登录用户id,并调用ThreadLocalset方法来设置当前线程的线程局部变量的值(用户id),然后在MyMetaObjectHandlerupdateFill方法中调用ThreadLocalget方法来获得当前线程所对应的线程局部变量的值(用户id)。

具体实现

  • 在com.blog.common包下新建BaseContext类
  • 作用:基于ThreadLocal的封装工具类,用于保护和获取当前用户id

    JAVA

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    public class BaseContext {
        private static ThreadLocal threadLocal = new ThreadLocal<>();
    
        public static void setCurrentId(Long id) {
            threadLocal.set(id);
        }
    
        public static Long getCurrentId() {
            return threadLocal.get();
        }
    }
    
  • 随后在LoginCheckFilter类中添加代码
    使用request.getSession来获取当前登录用户的id值

    JAVA

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    //4.判断登录状态,如果已登录,则直接放行
    if (request.getSession().getAttribute("employee") != null) {
        log.info("用户已登录,id为{}", request.getSession().getAttribute("employee"));
        //在这里获取一下线程id
        long id = Thread.currentThread().getId();
        log.info("doFilter的线程id为:{}", id);
        //根据session来获取之前我们存的id值
        Long empId = (Long) request.getSession().getAttribute("employee");
        //使用BaseContext封装id
        BaseContext.setCurrentId(empId);
        filterChain.doFilter(request, response);
        return;
    }
    
  • 在MyMetaObjectHandler类中,添加设置id的代码

    JAVA

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    @Component
    @Slf4j
    public class MyMetaObjectHandler implements MetaObjectHandler {
        @Override
        public void insertFill(MetaObject metaObject) {
            log.info("公共字段填充(create)...");
            metaObject.setValue("createTime", LocalDateTime.now());
            metaObject.setValue("updateTime", LocalDateTime.now());
            //设置创建人id
            metaObject.setValue("createUser", BaseContext.getCurrentId());
            metaObject.setValue("updateUser", BaseContext.getCurrentId());
        }
    
        @Override
        public void updateFill(MetaObject metaObject) {
            log.info("公共字段填充(insert)...");
            metaObject.setValue("updateTime", LocalDateTime.now());
            //设置更新人id
            metaObject.setValue("updateUser", BaseContext.getCurrentId());
        }
    }
    
  • 重新启动服务器,并登录一个非管理员账户,然后进行添加用户操作,观察数据库中的updateUser是否符合预期
    例如我这里登录的账号是Kyle,添加了Tony,Tony的create_user的id是Kyle的

id name username password phone sex id_number status create_time update_time create_user update_user
1575840690817011713 KKK Kyle e10adc3949ba59abbe56e057f20f883e 15811233075 1 111222333444555666 1 2022-10-05 17:02:53 2022-10-05 17:02:53 1 1
1577590825519423490 史塔克 Tony e10adc3949ba59abbe56e057f20f883e 15732165478 1 111333222444666555 1 2022-10-05 17:25:38 2022-10-05 17:25:38 1575840690817011713 1575840690817011713
  • 那么至此,公共字段填充功能,我们就完成了

 

你可能感兴趣的:(java)