瑞吉外卖业务开发

数据库

  • 我电脑上的数据库登录指令:mysql -uroot -p123456
  • 常用指令:show databases、user 数据库名、show tables。

创建项目

创建完项目后,要及时检查maven仓库的配置,jdk的配置,项目的编码,如下图。

瑞吉外卖业务开发_第1张图片

瑞吉外卖业务开发_第2张图片

瑞吉外卖业务开发_第3张图片

 配置项目的pom依赖和aplication文件,在启动类中加入lombok的slf4j注解,就可以使用log.info()方法在控制台输出信息了,方便调试。

映射静态资源

  • 问题:如果静态资源直接放入resource目录之下,而不是放在static或者templates目录下面,则项目启动后,浏览器无法直接访问到静态资源。
  • 解决方法,编写WebMvcConfig配置文件,文件所在目录以及内容如下所示。瑞吉外卖业务开发_第4张图片
package com.example.reggie_take_out;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@Slf4j
@SpringBootApplication
public class ReggieTakeOutApplication {

    public static void main(String[] args) {
        SpringApplication.run(ReggieTakeOutApplication.class, args);
        log.info("项目启动成功。。。");
    }

}

 
 

用户登录退出和拦截器功能的实现

用户登录功能

@PostMapping("/login")
    public R login(HttpServletRequest request, @RequestBody Employee employee){

        //1、将页面提交的密码password进行md5加密处理
        String password = employee.getPassword();
        password = DigestUtils.md5DigestAsHex(password.getBytes());

        //2、根据页面提交的用户名username查询数据库
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        //参数中,第一个参数是属性的引用,用于指定要匹配的属性。第二个参数是属性值,表示要匹配的值。
        queryWrapper.eq(Employee::getUsername, employee.getUsername());
        Employee emp = employeeService.getOne(queryWrapper);

        //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);
    }

用户退出

  @PostMapping("/logout")
    public R logout(HttpServletRequest request){
        //清理session中保存的员工ID
        request.getSession().removeAttribute("employee");
        return R.success("退出成功");
    }

拦截器

拦截器和过滤器各自适用的场景

拦截器通常在业务处理层面进行操作,它们更接近业务逻辑,可以对请求进行细粒度的控制和处理。例如,权限验证是一个常见的业务处理需求,拦截器可以拦截请求并检查用户的权限,以确保只有具有访问权限的用户可以执行相应的操作。另外,日志记录也是拦截器常见的应用场景,通过拦截请求和响应,可以记录请求的细节和响应的结果,方便问题的排查和系统的监控。

过滤器则更多地关注于请求和响应的处理和过滤。它们通常在请求的前后进行操作,用于对请求和响应进行过滤、修改或转换。请求过滤是过滤器的常见应用场景,可以用于对请求进行预处理、验证和过滤,例如检查请求的来源、请求的参数等。同时,过滤器还可以对请求和响应的编码进行转换,以确保请求和响应的正确编码格式。

综上所述,拦截器和过滤器在不同的层面和目的上有所不同,拦截器更偏向于业务处理和控制,而过滤器更专注于对请求和响应的处理和过滤。

//注意在启动类中添加注解 @ServletComponentScan
@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;
        log.info("拦截到请求:{}",request.getRequestURI());

        // 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 == true){
            filterChain.doFilter(request, response);
            return;
        }

        // 4、判断登录状态,如果已登录,则直接放行
        if(request.getSession().getAttribute("employee") != null){
            filterChain.doFilter(request, response);
            return;
        }

        // 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){
            if(PATH_MATCHER.match(url, requestURI) == true ){
                return true;
            }
        }
        return false;
    }
}
由于dofilter的返回类型为void,所以不能通过return R.error("错误信息")向客户端返回信息,可使用response对象向客户端返回信息:
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));

新增员工功能

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

        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("新增员工成功");
    }

员工查询

    @GetMapping("/page")
    public R page(int page, int pageSize, String name){
        log.info("分页查询{} {} {}", page, pageSize, name);

        //构造分页构造器
        Page pageInfo = new Page(page, pageSize);

        //构造条件构造器
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper();
        //添加过滤条件
        if(name != null){
            queryWrapper.like(Employee::getName, name);
        }
        //添加排序条件
        queryWrapper.orderByDesc(Employee::getUpdateTime);

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

需要注意的是,如果没有配置 MyBatis Plus 的分页插件,意味着分页功能将不会启用。 那么调用 employeeService.page(pageInfo, queryWrapper) 方法时,将无法进行分页查询, 而是会返回所有符合条件的结果,而不是按照指定的分页参数进行分页查询。

启用和禁用员工

    /**
     * 根据id修改员工信息
     * @param employee
     * @return
     */
    @PutMapping
    public R updata(HttpServletRequest request, @RequestBody Employee employee){
        log.info(employee.toString());
        long empid = (long)request.getSession().getAttribute("employee");
        employee.setUpdateUser(empid);
        employee.setUpdateTime(LocalDateTime.now());
        employeeService.updateById(employee);
        return R.success("员工信息修改成功");
    }

注意,ID为long类型,有19位,页面中js处理long型数字只能精确到前16位,所以最终通过ajax请求提交给服务端的时候id发生了变化。

解决办法是,将返回给客户端的数据转换为JSON格式,具体做法是在WebMvcConfig配置文件中添加扩展mvc框架的消息转换器,如下所示。

    /**
     * 扩展mvc框架的消息转换器
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List> converters) {

        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        //设置对象转换器  底层使用Jackson将Java对象转为json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        //将上面的消息转换器对象追加到mvc框架的转换器集合中
        converters.add(0, messageConverter);
        super.extendMessageConverters(converters);
    }

公共字段的填充

  • 在实体类的相应字段上加入如下的注解。
    //在插入时生效
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    //在插入和更新时生效
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
  • 在MyMetaObjecthandler类中无法获得HttpSession对象,我们用TreadLocal来解决该问题,他是JDK中提供的一个类。瑞吉外卖业务开发_第5张图片

瑞吉外卖业务开发_第6张图片

  •  如下为代码实现。
  • 首先构建基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
    package com.example.reggie_take_out.common;
    
    /**
     * 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
     */
    public class BaseContext {
        private static ThreadLocal threadLocal = new ThreadLocal<>();
    
        public static void setCurrentId(Long id){
            threadLocal.set(id);
        }
    
        public static Long getCurrenId(){
            return threadLocal.get();
        }
    }
    
  • 从filter中将用户id存入threadLocal提供的存储空间当中
        // 4、判断登录状态,如果已登录,则直接放行
        if(request.getSession().getAttribute("employee") != null){

            //将用户id存入threadLocal提供的存储空间当中
            BaseContext.setCurrentId((Long)request.getSession().getAttribute("employee"));

            filterChain.doFilter(request, response);
            return;
        }
  • 在MyMetaObjecthandler类中获取用户ID
        /**
         * 更新操作自动填充
         * @param metaObject
         */
        @Override
        public void updateFill(MetaObject metaObject) {
            log.info("公共字段自动填充[update]");
            log.info((metaObject.toString()));
            metaObject.setValue("updateTime", LocalDateTime.now());
            metaObject.setValue("updateUser", BaseContext.getCurrenId());
        }

 代码开发的结构

瑞吉外卖业务开发_第7张图片

删除分类

删除分类时要注意,判断被删除的分类是否关联了菜品或者套餐,因此就不能在CategoryController直接使用categoryService.removeById( id )对分类进行删除。

在CategoryService接口中定义remove方法,实现关联删除的逻辑业务判断,在CategoryServiceImpl中具体的实现方法如下。

@Service
public class CategoryServiceImpl extends ServiceImpl implements CategoryService {

    @Autowired
    private DishService dishService;

    @Autowired
    private SetmealService setmealService;

    /**
     * 根据id进行删除,删除之前要做判断
     * @param id
     */
    @Override
    public void remove(Long id) {
        //查询当前分类是否关联菜品,如果已经关联,抛出一个业务异常
        LambdaQueryWrapper dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
        dishLambdaQueryWrapper.eq(Dish::getCategoryId, id);
        int count1 = dishService.count(dishLambdaQueryWrapper);
        if(count1 >0) {
            throw new CustomException("当前分类下关联了菜品,不能删除");
        }


        //查询当前分类是否关联套餐,如果已经关联,抛出一个业务异常
        LambdaQueryWrapper setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
        setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId, id);
        int count2 = setmealService.count(setmealLambdaQueryWrapper);
        if(count2 >0) {
            throw new CustomException("当前分类下关联了套餐,不能删除");

        }
        //正常删除业务
        super.removeById(id);
    }
}

自定义异常类的代码如下。

/**
 * 自定义业务异常类
 */
public class CustomException extends RuntimeException {
    public CustomException(String message){
        super(message);
    }
}

全局异常处理器的代码如下。

    /**
     * 异常处理方法
     * @return
     */
    @ExceptionHandler(CustomException.class)
    public R exceptionHandler(CustomException ex){
        log.error(ex.getMessage());

        return R.error(ex.getMessage());
    }

文件上传

    /**
     * 文件上传
     * @param file
     * @return
     */
    @PostMapping("/upload")
    public R upLoad(MultipartFile file){
        //file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会被删除
        log.info(file.toString());

        //原始文件名
        String originalFilename = file.getOriginalFilename();
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));

        //使用UUID重新生成文件名,防止文件名称重复造成覆盖
        String filename = UUID.randomUUID().toString() + suffix;

        //创建一个目录对象
        File dir = new File(basePath);
        if(!dir.exists()){
            dir.mkdir();
        }

        //将临时文件转存到指定位置
        try {
            file.transferTo(new File(basePath+filename));
        } catch (IOException e) {
            e.printStackTrace();
        }

        return R.success(filename);
    }

文件下载

 /**
     * 文件下载
     * @param name
     * @param response
     */
    @GetMapping("/download")
    public void download(String name, HttpServletResponse response){
        //输入流,通过输入流读取文件内容
        try {
            FileInputStream fileInputStream = new FileInputStream(basePath + name);

            //输出流,通过输出流将文件写回浏览器,在浏览器展示图片
            ServletOutputStream outputStream = response.getOutputStream();

            response.setContentType("/image/jpeg");

            //用于存储文件内容的缓冲区。
            int len = 0;
            byte[] bytes = new byte[1024];

            //输入流中读取文件内容,并将其写入输出流,直到文件的所有内容都被读取完毕。
            while( (len = fileInputStream.read(bytes)) != -1){
                //这行代码将缓冲区中的内容写入输出流,并通过flush()方法将数据刷新到浏览器。
                outputStream.write(bytes, 0, len);
                outputStream.flush();
            }

            //关闭资源
            outputStream.close();
            fileInputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

新增菜品

新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish,dish_flavor。

因此在DishService接口中自定义方法void saveWithFlavor(DishDto dishDto),用于新增菜品,同时插入菜品对应的口味数据。DishServiceImpl实现类中的代码如下。

@Service
@Slf4j
public class DishServiceImpl extends ServiceImpl implements DishService {

    @Autowired
    private DishFlavorService dishFlavorService;

    /**
     * 新增菜品,同时保存相应的口味数据
     * @param dishDto
     */
    @Override
    @Transactional
    public void saveWithFlavor(DishDto dishDto) {
        //this.save(dishDto) 将调用 DishServiceImpl 类中的 save 方法,将 dishDto 对象保存到数据库中。
        this.save(dishDto);

        Long id = dishDto.getId();//菜品id

        //菜品口味
        List flavors = dishDto.getFlavors();

        //赋值菜品口味的相应id
        flavors = flavors.stream().map((item) -> {
            item.setDishId(id);
            return item;
        }).collect(Collectors.toList());

        dishFlavorService.saveBatch(flavors);
    }
}

注意,saveWithFlavor方法中操作了两张表,因此需要方法上加入@Transactional注解,并且在启动类中加上@EnableTransactionManagement 注解。

菜品展示

问题:页面展示需要分类的名称,但dish表中只有分类的id,因此总体思路是将dish表中的分类id取出来,用分类id在分类表中查询分类名称。

具体步骤:

  • 使用DTO(Data Transfer Object)数据传输对象,用于在不同层之间传输数据。DTO结构如下。
    @Data
    public class DishDto extends Dish {
    
        private List flavors = new ArrayList<>();
    
        private String categoryName;
    
        private Integer copies;
    }

    DTO继承自Dish类,并且DTO的categoryName属性可用于前端分类名称的展示。

你可能感兴趣的:(数据库,mysql,sql)