外卖项目Note

  • 流程:
    在这里插入图片描述

对密码进行加密

... = DigestUtils.md5DigestAsHex(password.getBytes());

对象拷贝

利用BeanUtils.copyProperties(... , ...)

stream流:

例如:

		List<DishDto> dishDtoList = records.stream().map(item->{
            DishDto dishDto = new DishDto();
            // 2.2 拷贝
            BeanUtils.copyProperties(item,dishDto);

            Long categoryId = item.getCategoryId();
            Category category = categoryService.getById(categoryId);
            if (category!=null){
                //2.3 设置实体类DishDto的categoryName属性值
                String categoryName = category.getName();
                dishDto.setCategoryName(categoryName);
            }
            return dishDto;
        }).collect(Collectors.toList());

3.公共字段自动填充

Mybatis Plus提供的公共字段自动填充功能,也就是在插入或更新时给指定字段赋予指定的值

  • 优点:统一处理这些字段,避免重复代码。
  • 公共字段自动填充实现步骤:
    • 3.1 在实体类的属性上添加@TableField注解,指定自动填充的策略
    • 3.4 按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需实现MetaObjectHandler接口;
  • 元数据对象处理器类中怎样获取用户id? (此类中是不能获得HttpSession对象的)
    • 使用ThreadLocal(JDK提供的一个类 )
      在这里插入图片描述
    • 获取登录用户id实现步骤:
      • 3.2 编写BaseContext工具类,基于ThreadLocal封装的工具类;
      • 3.3 在LoginCheckFilter中的doFilter方法中调用BaseContext来设置当前登录用户的id;
      • 3.4 在MyMetaObjectHandler的方法中调用BaseContext来获取当前登录用户id

3.1实体类的属性上添加@TableField注解

指定自动填充的策略

	@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;

3.2 BaseContext工具类:

基于ThreadLocal封装的工具类

package com.lym.reggie.common;

//基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
public class BaseContext {
    //用来存储用户id
    private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    //设置值
    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }
    
    // 获取值
    public static Long getCurrentId(){
        return threadLocal.get();
    }
}

3.3 设置当前登录用户的id:

在LoginCheckFilter中的doFilter方法中调用BaseContext来设置当前登录用户的id

Long id = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(id);

3.4 元数据对象处理器:

  • 按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需实现MetaObjectHandler接口;
  • 在方法中调用BaseContext来获取当前登录用户id
package com.lym.reggie.common;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;

@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        //调用BaseContext来获取当前登录用户id
        Long UserId = BaseContext.getCurrentId();
        log.info("UserId:{}",UserId);
        metaObject.setValue("createUser",UserId);
        metaObject.setValue("updateUser",UserId);
        log.info("元数据 insertFill");
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        metaObject.setValue("updateTime", LocalDateTime.now());
        //调用BaseContext来获取当前登录用户id
        Long UserId = BaseContext.getCurrentId();
        log.info("UserId:{}",UserId);
        metaObject.setValue("updateUser",UserId);

       /* long threadId = Thread.currentThread().getId();
        log.info("元数据 updateFill :ThreadLocal获得的id:{}",threadId);*/
    }
}

4. 分页查询

4.1、一张表的分页查询

  • 使用Mybatis-plus提供的分页插件,可简化分页查询代码量
1) 配置分页插件

config包下创建
配置MP的分页插件

package com.lym.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 = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

2) …Controller中

使用Page 这个泛型(…mybatisplus.extension.plugins.pagination包下的)

  • 1)构造分页构造器(Page)
    … = new Page(页码,pageSize);
  • 2)构造条件构造器
    2.1) LambdaQueryWrapper<实体>… = new LambdaQueryWrapper(); 或 用QueryWrapper
    2.2) 再添加条件
  • 3)执行查询
    …Service调用page方法,不需返回,因为查询内部就会将封装给page对象
@RequestMapping("/page")
    public R<Page> page(int page,int pageSize,String name){
        log.info("page:{},pageSize:{},name:{}",page,pageSize,name);
        // 1.构造分页
        Page<Employee> pageInfo = new Page<>(page,pageSize);
        // 2.构造条件
        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(name!=null,Employee::getName,name);
        //添加一个排序条件
        queryWrapper.orderByDesc(Employee::getUpdateTime);
        // 3.执行查询
        employeeService.page(pageInfo,queryWrapper);

        return R.success(pageInfo);
    }

4.2、涉及两张表的 分页查询

  • 对象拷贝:利用BeanUtils.copyProperties(... , ...)
  • stream流:records.stream().map(item->{。。。}.collect(Collectors.toList());

菜品信息分页查询:

//分页查询
    @GetMapping("/page")
    public R<Page> page(int page,int pageSize,String name){
        log.info("name: {}",name);
      // 1. Dish分页查询
        //1.1 构造一个分页构造器对象
        Page<Dish> dishPage = new Page<>(page,pageSize);
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(name!=null,Dish::getName,name);
        queryWrapper.orderByDesc(Dish::getUpdateTime);
        dishService.page(dishPage,queryWrapper);

        Page<DishDto> dishDtoPage = new Page<>();
      //   2. 对象拷贝:使用框架自带的工具类,第三个参数是不拷贝到属性 records属性是分页插件中表示分页中所有的数据的一个集合
        BeanUtils.copyProperties(dishPage,dishDtoPage,"records");

        // 2.1 处理 再进行拷贝
        List<Dish> records = dishPage.getRecords();
        List<DishDto> dishDtoList = records.stream().map(item->{
            DishDto dishDto = new DishDto();
            // 2.2 拷贝
            BeanUtils.copyProperties(item,dishDto);

            Long categoryId = item.getCategoryId();
            Category category = categoryService.getById(categoryId);
            if (category!=null){
                //2.3 设置实体类DishDto的categoryName属性值
                String categoryName = category.getName();
                dishDto.setCategoryName(categoryName);
            }
            return dishDto;
        }).collect(Collectors.toList());
        //  3. 重新设置records属性值
        dishDtoPage.setRecords(dishDtoList);
        return R.success(dishDtoPage);
    }

6. 文件上传和下载

6.1 文件上传

  • 将文件上传到服务器
  • 文件上传代码逻辑
    • 0、修改yml配置文件:配置上传图片的存储位置;
    • 1、生成一个新的 唯一的文件名 (利用UUID) 以防止文件名相同造成覆盖;
    • 2、判断要接收文件的目录是否存在,若不存在则创建目录对象。
    • 3、把前端传入的文件进行转存。
  • 服务端要接收客户端页面上传的文件,通常需要使用Apache的两个组件:
    • commons-fileupload
    • commons-io
  • Spring框架在spring-web包中对文件上传进行了封装,简化了服务端代码,我们只需要在Controller的方法中声明MultipartFile类型的参数就可以接收上传的文件。

Alt
Alt

  • 文件上传概述:
    Alt
    Alt

6.2 文件下载

  • 将文件从服务器传送到本地计算机。
  • 步骤:
    • 1。输入流。因为需要通过输入流读取服务器的文件。
    • 2。设置写回去的文件类型。设置响应的数据格式;
    • 3。输出流,需要通过输出流将文件写回浏览器;
    • 4。定义缓存区,准备读写文件;
    • 5。关流。

Alt
Alt

6.3 后端代码实现

  1. yml配置文件:配置上传图片的存储位置;
  2. 代码:上传、下载
  • yml配置文件:配置上传图片的存储位置;
reggie:
  path: /Users/liangyuanmeng/Desktop/JavaFile/Project/new/reggieImg/
  • 代码:上传、下载
import com.lym.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.UUID;

@RestController
@Slf4j
@RequestMapping("/common")
public class CommonController {
    @Value("${reggie.path}")
    private String basePath;

    //文件上传. 将文件上传到服务器
    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){
        //1。生成一个新的 唯一的文件名 (利用UUID) 以防止文件名相同造成覆盖
        //1.1 获取后缀,比如 .png  .jpg
        String originName = file.getOriginalFilename();//文件原始名
        log.info("文件原始名:{}",originName);
        String suffix = originName.substring(originName.lastIndexOf('.'));

        //1.2 使用uuid生成的作为文件名的一部分,这样可以防止文件名相同造成的文件覆盖
        String fileName = UUID.randomUUID().toString() + originName;

        //2。判断要接收文件的目录是否存在,若不存在则创建目录对象。
        File dir = new File(basePath);
        if (!dir.exists()){
            //文件目录不存在,直接创建一个目录
            dir.mkdirs();
        }

        //3。把前端传入的文件进行转存。
        try {
            //把前端传过来的文件进行转存
            file.transferTo(new File(basePath+fileName));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return R.success(fileName);
    }
    
    //下载。 将文件从服务器传送到本地计算机
    @GetMapping("/download")
    public void download(String name, HttpServletResponse response){
        try {
            //1。输入流,通过输入流读取服务器的文件。这里的name是前台用户需要下载的文件的文件名
            FileInputStream inputStream = new FileInputStream(new File(basePath+name));

            //2。设置写回去的文件类型。设置响应的数据格式
            response.setContentType("image/jpeg");

            //3。输出流,通过输出流将文件写回浏览器
            ServletOutputStream outputStream = response.getOutputStream();

            //4。定义缓存区,准备读写文件
            int len = 0;
            byte[] buff = new byte[1024];
            while ((len = inputStream.read(buff))!=-1){
                outputStream.write(buff,0,len);
                outputStream.flush();
            }

            //5。关流
            outputStream.close();
            inputStream.close();
            
        } catch (Exception e) {
            e.printStackTrace();
        }
}

Note:这里上传的文件的文件名要和这个地方的一样,接收文件的参数的名不能随便定义,要和下面的name的值一致;
Alt

1. 完善登录功能

token:
https://blog.csdn.net/tolode/article/details/103788487

  • 如何实现?
    使用过滤器或者是拦截器,在拦截器或者是过滤器中判断用户是否已经完成了登陆,如果没有登陆则跳转到登陆页面;
  • 方案:
    • 使用过滤器
    • 使用拦截器拦截器
    • Spring Security权限控制 ?

1.1 利用过滤器

1.1.1 自定义过滤器 -->检查用户是否已完成登录
1)处理逻辑
过滤器的具体处理逻辑:
1.1 获取本次请求的URI      request
1.2 定义不需处理的请求路径
   登录、登出、页面
  (只拦截除登录登出外的 Controller的请求)
1.3 判断本次请求是否需要处理
   1.3.1 如果不需要处理,则直接放行
1.4 判断登陆状态
   1.4.1 如果已登录,则直接放行
   1.4.2 如果未登录则返回未登录结果
       (如果方法没有返回值,则通过输出流给前端提供json数据)
2)过滤器代码
package com.lym.reggie.filter;

import com.alibaba.fastjson.JSON;
import com.lym.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;

/**
 * 检查用户是否已经完成登录
 * filterName过滤器名字
 * urlPatterns拦截的请求,这里是拦截所有的请求
 */
@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 {
        // 0 对请求和响应进行强转,我们需要的是带http的
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

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

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

        // 定义不需要处理的请求路径  登录、登出、页面(只拦截除登录登出外的 Controller的请求)(静态页面我们不需要拦截,因为此时的静态页面是没有数据的)
        String[] urls = new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**"
        };


        //2、判断本次请求是否需要处理
        boolean check = check(urls, requestURI);

        //2.1 如果不需要处理,则直接放行
        if(check){
            log.info("本次请求{}不需要处理",requestURI);
            filterChain.doFilter(request,response);
            return;
        }

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

        log.info("用户未登录");
        //4、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
        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) {
            //把浏览器发过来的请求和我们定义的不拦截的url做比较,匹配则放行
            boolean match = PATH_MATCHER.match(url, requestURI);
            if(match){
                return true;
            }
        }
        return false;
    }
}

1.1.2 启动类上加注解:@ServletComponentScan

开启组件扫描,这样才会去扫描@WebFilter注解,才能扫描到过滤器

2. 异常捕获

  • 需要程序进行异常捕获时,有两种解决方案:
    • Controller方法中加try catch进行异常捕获
    • 使用异常处理器进行全局异常捕获

2.1 异常处理器方式

  • 底层是基于代理
1)新增时
  • 例子:employee表中对username字段加了唯一约束,在新增员工时,如果新增的账户已经存在,程序会抛出SQLIntegrityConstraintViolationException异常。这时我们就可以使用全局异常处理器来捕获,进行处理。
    在这里插入图片描述

(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 org.springframework.web.bind.annotation.RestController;
import java.sql.SQLIntegrityConstraintViolationException;

/**
 * 全局异常处理
 */
@ControllerAdvice(annotations = {RestController.class, Controller.class})//表示拦截哪些类型的controller注解
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 异常处理方法,处理SQLIntegrityConstraintViolationException异常的方法
     * @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("未知错误");
    }
}

2)删除 (存在关联情况时)
  • 可能需要删除的数据是与其他表关联的,所以删除之前要先判断该条数据是否与其他表中的数据关联;
  • 例如:删除菜品分类时
    • 需检查要删除的菜品分类是否关联了 菜品或套餐:
      • 1)若未关联,则删除;
      • 2)若关联了,则不删除
        通过自定义异常实现:若关联则抛出异常,然后使用异常处理器进行全局异常捕获,然后把异常信息显示给前端人员。
    • 实现步骤:
      • 1)自定义一个异常类;
      • 2)如果要删除的数据与其他表不关联,就直接删除。
      • 3)如果存在关联,就抛出自定义的异常。
      • 4)在GlobalExceptionHandler全局异常捕获器中添加这个自定义异常,统一进行异常处理,再响应给客户端。
  1. 自定义异常;
package com.lym.my_reggie.common.exception;

public class CustomException extends RuntimeException {
    /**
     * 异常构造方法 在使用时方便传入错误码和信息
     */
    public CustomException(String msg) {
        super(msg);
    }
}
  1. 3.实现类中 若无关联则删除,若关联则抛出自定义的异常;
/**
     * 根据id删除 分类,删除之前需要进行判断是否有关联数据
     * @param id
     */
    @Override
    public void remove(Long id) {
        LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
        //添加查询条件
        dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
        //注意:这里使用count方法的时候一定要传入条件查询的对象,否则计数会出现问题,计算出来的是全部的数据的条数
        int count = dishService.count(dishLambdaQueryWrapper);
 
        //查询当前分类是否关联了菜品,如果已经管理,直接抛出一个业务异常
        if (count > 0){
            //已经关联了菜品,抛出一个业务异常
            throw new CustomException("当前分类项关联了菜品,不能删除");
        }
 
        //查询当前分类是否关联了套餐,如果已经管理,直接抛出一个业务异常
        LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
        setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
        //注意:这里使用count方法的时候一定要传入条件查询的对象,否则计数会出现问题,计算出来的是全部的数据的条数
        int setmealCount = setmealService.count(setmealLambdaQueryWrapper);
        if (setmealCount > 0){
            //已经关联了套餐,抛出一个业务异常
            throw new CustomException("当前分类项关联了套餐,不能删除");
        }
        //正常删除
        super.removeById(id);
 
    }
  1. GlobalExceptionHandler全局异常捕获器中添加这个自定义异常;当抛出异常时,在GlobalExceptionHandler全局异常捕获器中捕获异常,统一进行异常处理,再响应给客户端。
//处理自定义的异常,为了让前端展示我们的异常信息,这里需要把异常进行全局捕获,然后返回给前端
    @ExceptionHandler(CustomException.class)
    public R<String> exceptionHandle(CustomException ex){
        log.error(ex.getMessage());
        //这里拿到的message是业务类抛出的异常信息,我们把它显示到前端
        return R.error(ex.getMessage());
    }
怎样实现的统一捕获异常?异常捕获器 怎样实现的捕获到异常?❓❓

5. 前后端 传递id不匹配问题

基于Jackson进行Java对象到json数据的转换。

当id生成策略为ASSIGN_ID时(雪花算法),对id使用了雪花算法,所以存入数据库中的id是19为长度,但是前端的js只能保证数据的前16位的数据的精度,对我们id后面三位数据进行了四舍五入,所以就出现了精度丢失;就会出现前端传过来的id和数据里面的id不匹配,就没办法正确的修改到我们想要的数据;

  • 解决方案:
    • 5.1)使用自增ID的策略往数据库添加id
    • 5.2)使用自定义消息转换器
      将long型的数据统一转换为string字符串

5.2)使用自定义消息转换器

既然js对long型的数据会进行精度丢失,那么我们就对数据进行转型,我们可以在服务端(Java端)给页面响应json格式的数据时进行处理,将long型的数据统一转换为string字符串

  • 实现步骤:
    • 1)创建对象(消息)转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换;
    • 2)在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换。
1)自定义消息(对象)转换类
package com.itheima.reggie.common;
 
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)//将long型转为String字符串
                .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);
    }
}
2)在前面的webMvcConfig 配置类中扩展spring mvc 的消息转换器,在此消息转换器中使用spring提供的对象转换器进行Java对象到json数据的转换;

(底层本来就有转换器,但是不符合我们的需求,因此我们自己写一个转换器,但是要加入到他们的数组中才统一管理才可以生效)

(Controller方法结果转为json过程,会使用到对象转换器,底层就是使用Jackson )

  1. 重写extendMessageConverters(...)方法;
  2. 方法中:
    (1)创建一个消息转换器,并设置对象转换器为自定义的对象转换器
    (2)将此消息转换器追加到mvc框架的转换器集合中,并设置自定义的消息转换器为第一优先级。这样就会优先使用我们的转换器来进行相关数据进行转换,如果我们的转换器没有匹配到相应的数据来转换,那么就会去寻找第二个优先级的转换器,以此类推
     /**
     * 扩展mvc框架的消息转换器
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        //log.info("扩展消息转换器...");
        //创建消息转换器对象。 作用:将Controller方法的返回结果转为json,再通过输出流的方式响应给页面
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        //设置对象转换器,底层使用Jackson将Java对象转为json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        
        //将上面的消息转换器对象追加到mvc框架的转换器集合中
        //转换器是有优先级顺序的,这里我们把自己定义的消息转换器设置为第一优先级,所以会优先使用我们的转换器来进行相关数据进行转换,如果我们的转换器没有匹配到相应的数据来转换,那么就会去寻找第二个优先级的转换器,以此类推。所以这里第一个参数是0
        converters.add(0,messageConverter);
    }

7. 新增

7.1 新增时 出现异常时 ->详见2.

7.2 新增 涉及多张表时

  • 例子:新增菜品,涉及两个表

    • dish(菜品表):将新增页面录入的菜品信息插入到dish表;
    • dish_flavor(菜品口味表):若添加了口味信息,将口味信息插入dish_flavor表。
  • 解决步骤:
    回显数据 及 图片上传下载此处省略

    • 1、创建一个DishDto用于封装页面提交的数据;(也可以使用map来接收)
    • 2、在DishService中新增一个方法:saveWithFlavor(DishDto dishDto);
    • 3、实现此方法 、@Transactional
      • 方法上加@Transactional(因为涉及对多张表的数据进行操作,需要加事务)
      • 实现步骤:
        • 1)保存dish信息到dish表;
        • 2)保存口味信息到dish_flavors表;
          • 通过遍历将dishId设置进flavors;
          • 把菜品口味的数据保存到dish_flavors口味表;
    • 4、在启动类开启事务: 加上 @EnableTransactionManagement注解
    • 5、写Controller层代码
  • 1、创建一个DishDto用于封装页面提交的数据;(也可以使用map来接收)
    在这里插入图片描述

  • 2、在DishService中新增一个方法:

//新增菜品,同时插入菜品对应的口味数据,需要同时操作两张表:dish  dish_flavor
void saveWithFlavor(DishDto dishDto);
  • 3、实现:
@Autowired
private DishFlavorService dishFlavorService;
/**
 * 新增菜品同时保存对应的口味数据
 * @param dishDto
 */
@Override
@Transactional //涉及到对多张表的数据进行操作,需要加事务,需要事务生效,需要在启动类加上事务注解生效
public void saveWithFlavor(DishDto dishDto) {
    	//1。保存dish信息到dish表
        this.save(dishDto);
        Long dishId = dishDto.getId();
        //2。保存口味信息到dish_flavors表
        //2.1 通过遍历将dishId设置进flavors
        List<DishFlavor> flavors = dishDto.getFlavors();
        // stream流
        flavors = flavors.stream().map(item->{
            //拿到的这个item就是这个DishFlavor集合
            item.setDishId(dishId);
            return item;
        }).collect(Collectors.toList());//把返回的集合搜集起来,用来被接收

        //把菜品口味的数据到口味表 dish_flavor  注意dish_flavor只是封装了name value 并没有封装dishId(从前端传过来的数据发现的,然而数据库又需要这个数据)
        dishFlavorService.saveBatch(dishDto.getFlavors());//这个方法是批量保存
}
  • 4、在启动类开启事务: 加上 @EnableTransactionManagement注解
  • 5、Controller中:
//新增菜品
    @PostMapping
    public R<String> addDish(@RequestBody DishDto dishDto){//前端提交的是json数据的话,我们在后端就要使用这个注解来接收参数,否则接收到的数据全是null
        dishService.saveWithFlavor(dishDto);
        return R.success("新增菜品成功");
    }

8. 下单

  • AtomicInteger:原子整数类,保证线程安全。一个提供原子操作的Integer的类。在Java语言中,++i和i++操作并不是线程安全的,在使用的时候,不可避免的会用到synchronized关键字。而AtomicInteger则通过一种线程安全的加减操作接口。
  • BigDecimal:BigDecimal类不能直接用
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值