瑞吉外卖业务开发

 创建web项目

 热部署

 

 Web入门

  •  Spring Boot将传统Web开发的mvc、json、tomcat等框架整合,提供了 spring-boot-starter-web组件,简化了Web应用配置。
  • webmvc为Web开发的基础框架,json为JSON数据解析组件,tomcat为自带的容器依赖

控制器

RestController控制器(常用)

负责和接受HTTP请求,如果浏览器请求的只是数据,使用RestController即可。默认情况下,@RestController注解会将返回的对象数据转换为JSON格式。

Controller控制器

负责接收和处理HTTP请求。 如果请求的是页面和数据,使用@Controller注解即可 。

如下代码中返回了hello页面和name的数据,在前端页面中可以通过${name}参数 获取后台返回的数据并显示。  @Controller通常与Thymeleaf模板引擎结合使用。

 URL映射和参数传递

@RestController
public class ParamsContorller {

    @RequestMapping(value = "/getTest1", method = RequestMethod.GET)
    public String getTest1(){
        return "GET请求";
    }

    @RequestMapping(value = "/getTest2", method = RequestMethod.GET)
    public String getTest2(String nickname){
        System.out.println(nickname);
        return "GET请求";
    }

    @RequestMapping(value = "/postTest1", method = RequestMethod.POST)
    public  String postTest1(){
        return "POST请求";
    }

    @RequestMapping(value = "/postTest2", method = RequestMethod.POST)
    public  String postTest2(String nickname){
        System.out.println("nickname:"+nickname);
        return "POST请求";
    }

    @RequestMapping(value = "/postTest3", method = RequestMethod.POST)
    public  String postTest3(User user){
        System.out.println("User:"+user);
        return "POST请求";
    }

    @RequestMapping(value = "/postTest4", method = RequestMethod.POST)
    public  String postTest4(@RequestBody User user){  // 使用json格式请求
        System.out.println("User:"+user);
        return "POST请求";
    }
}

数据库

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

检查项目

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

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

映射静态资源

  • 问题:如果静态资源直接放入resource目录之下,而不是放在static或者templates目录下面,则项目启动后,浏览器无法直接访问到静态资源。
  • 解决方法,编写WebMvcConfig配置文件,文件所在目录以及内容如下所示。
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<Employee> login(HttpServletRequest request, @RequestBody Employee employee){

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

        //2、根据页面提交的用户名username查询数据库
        LambdaQueryWrapper<Employee> 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<String> 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<String> 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> page(int page, int pageSize, String name){
        log.info("分页查询{} {} {}", page, pageSize, name);

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

        //构造条件构造器
        LambdaQueryWrapper<Employee> 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<String> 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<HttpMessageConverter<?>> 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中提供的一个类。

  •  如下为代码实现。
  • 首先构建基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
    package com.example.reggie_take_out.common;
    
    /**
     * 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
     */
    public class BaseContext {
        private static ThreadLocal<Long> 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());
        }

 代码开发的结构

删除分类

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

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

@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {

    @Autowired
    private DishService dishService;

    @Autowired
    private SetmealService setmealService;

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


        //查询当前分类是否关联套餐,如果已经关联,抛出一个业务异常
        LambdaQueryWrapper<Setmeal> 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<String> exceptionHandler(CustomException ex){
        log.error(ex.getMessage());

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

文件上传

    /**
     * 文件上传
     * @param file
     * @return
     */
    @PostMapping("/upload")
    public R<String> 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<DishMapper, Dish> 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<DishFlavor> 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<DishFlavor> flavors = new ArrayList<>();
    
        private String categoryName;
    
        private Integer copies;
    }

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

新增套餐

  • 传入的Json数据,不仅有套餐数据,还有对应套餐的菜品数据,因此需要两个实体来接受客户端发送的Json数据,分别是套餐实体Setmeal和套餐菜品实体SetmealDish,创建一个setmealDto实体,Dto表继承自套餐实体,并将元素类型为套餐菜品实体的list集合作为字段属性。
  • 然后在套餐的service中,分别保存两张表,分别是套餐表和套餐菜品表。
  • 对于套餐表的保存,直接setmealService.save(setmealDto)即可,因为Dto实体直接继承自套餐实体。
  • 对于套餐菜品表的保存,需要先将套餐实体的id赋给Dto实体中每一个套餐菜品实体,然后再通过setmealDishService.saveBatch(setmealDishs)。
        @Autowired
        private SetmealDishService setmealDishService;
    
        /**
         * 新增套餐,同时需要保存套餐和菜品的关联关系
         * @param setmealDto
         */
        @Transactional  //两张表  要么全部成功  要么全部失败
        public void saveWithDish(SetmealDto setmealDto) {
            //保存套餐的基本信息,操作setmeal,执行insert操作
            this.save(setmealDto);
    
            List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
            setmealDishes.stream().map((item) -> {
                item.setSetmealId(setmealDto.getId()); //赋值套餐id
                return item;
            }).collect(Collectors.toList());
    
            //保存套餐和菜品的关联信息,操作setmeal_dish,执行insert操作
            setmealDishService.saveBatch(setmealDishes);
        }

套餐分页查询

  • 因为套餐实体Setmeal中的分类名称为id值,因此要用SetmealDto数据传输对象(Data Transfer Object),Dto继承自套餐实体,并且有分类名称字段。
  • 构造分页对象,分别是Setmeal分页构造器对象和SetmealDto分页构造器对象。
  • 创建LambdaQueryWrapper对象,添加查询条件,根据name进行like模糊查询,添加排序条件,根据更新时间降序排列,调用setmealService的page方法进行分页查询。
  • 对象拷贝,将pageInfo对象拷贝到dtoPage对象,但pageInfo对象的records列表的元素类型为Setmeal,而我们需要的元素类型为SetmealDto,因此records字段不需要被拷贝。
  • 为了使dtoPage的records列表字段的元素类型为SetmealDto,我使用流式操作对pageInfo的records列表进行处理。具体处理为创建SetmealDto对象,将Setmeal拷贝到SetmealDto对象,再根据分类id查询分类对象,然后将分类名称赋给SetmealDto对象。
  • 处理完成后,得到一个以SetmealDto为元素类型的list集合,将该集合赋给dtoPage的records字段,最终将dtoPage对象返回给客户端即可。
  • 代码如下。
        /**
         * 套餐分页查询
         * @param page
         * @param pageSize
         * @param name
         * @return
         */
        @GetMapping("/page")
        public R<Page> page(int page, int pageSize, String name){
            //分页构造器对象
            Page<Setmeal> pageInfo = new Page<>(page,pageSize);
            Page<SetmealDto> dtoPage = new Page<>();
    
            LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
            //添加查询条件,根据name进行like模糊查询
            queryWrapper.like(name != null,Setmeal::getName,name);
            //添加排序条件,根据更新时间降序排列
            queryWrapper.orderByDesc(Setmeal::getUpdateTime);
    
            setmealService.page(pageInfo,queryWrapper);
    
            //对象拷贝
            BeanUtils.copyProperties(pageInfo,dtoPage,"records");
            List<Setmeal> records = pageInfo.getRecords();
    
            List<SetmealDto> list = records.stream().map((item) -> {
                SetmealDto setmealDto = new SetmealDto();
                //对象拷贝
                BeanUtils.copyProperties(item,setmealDto);
                //分类id
                Long categoryId = item.getCategoryId();
                //根据分类id查询分类对象
                Category category = categoryService.getById(categoryId);
                if(category != null){
                    //分类名称
                    String categoryName = category.getName();
                    setmealDto.setCategoryName(categoryName);
                }
                return setmealDto;
            }).collect(Collectors.toList());
    
            dtoPage.setRecords(list);
            return R.success(dtoPage);
        }

套餐删除

  • 删除两张表,需要在service层中处理该逻辑,并且加上@Transaction注解,启动类加上@EnableTransactionManagement注解。
  • 处理逻辑为先判断表中是否有待删除但status为1的套餐,若有,则返回异常,反之正常删除套餐表中的待删除套餐。
  • 再使用LambdaQueryWrapper从套餐菜品表中查询对应套餐id的记录,执行service.remove操作删除对应的记录即可。
        /**
         * 删除套餐,同时需要删除套餐和菜品的关联数据
         * @param ids
         */
        @Transactional  //删除两张表
        public void removeWithDish(List<Long> ids) {
            //select count(*) from setmeal where id in (1,2,3) and status = 1
            //查询套餐状态,确定是否可用删除
            LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper();
            queryWrapper.in(Setmeal::getId,ids);
            queryWrapper.eq(Setmeal::getStatus,1);
    
            int count = this.count(queryWrapper);
            if(count > 0){
                //如果不能删除,抛出一个业务异常
                throw new CustomException("套餐正在售卖中,不能删除");
            }
    
            //如果可以删除,先删除套餐表中的数据---setmeal
            this.removeByIds(ids);
    
            //delete from setmeal_dish where setmeal_id in (1,2,3)
            LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
            lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);
            //删除关系表中的数据----setmeal_dish
            setmealDishService.remove(lambdaQueryWrapper);
        }

功能开发流程(以购物车开发为例)

  • 需求分析
  • 数据模型
  • 代码开发---梳理前后交互过程
  • 代码开发---准备工作
  • 代码开发(略)
  • 功能测试(略)

用户下单

  • 操作多张表,在service接口中扩展方法。
  • 业务逻辑如下:
  • 查询用户id,根据用户id获得用户数据和购物车数据,根据客户端传来的收货地址查询地址数据,使用IdWorker.getId() 方法创建一个订单号。
  • 对购物车数据创建一个Stream流,使用map方法对流中的每个元素进行映射操作,设置订单明细实体类的值,并计算订单的总额,最终向订单明细表插入数据。
  • 设置订单实体类的值,其订单金额由上一步计算得来,最终向订单表插入数据。
  • 最后删除购物车数据。
  • 代码如下。
        @Autowired
        private ShoppingCartService shoppingCartService;
    
        @Autowired
        private UserService userService;
    
        @Autowired
        private AddressBookService addressBookService;
    
        @Autowired
        private OrderDetailService orderDetailService;
    
        /**
         * 用户下单
         * @param orders
         */
        @Transactional
        public void submit(Orders orders) {
            //BaseContext获取当前用户id
            Long userId = BaseContext.getCurrentId();
    
            //查询当前用户的购物车数据
            LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(ShoppingCart::getUserId, userId);
            List<ShoppingCart> shoppingCarts = shoppingCartService.list(wrapper);
    
            if(shoppingCarts == null || shoppingCarts.size() == 0){
                throw new CustomException("购物车为空,不能下单");
            }
    
            //查询用户数据
            User user = userService.getById(userId);
    
            //查询地址数据
            AddressBook addressBook = addressBookService.getById(orders.getAddressBookId());
    
            if(addressBook == null){
                throw new CustomException("地址信息为空,不能下单");
            }
    
            long orderId = IdWorker.getId();  //订单号
    
            AtomicInteger amount = new AtomicInteger(0); //金额  线程安全
    
            //向订单明细表插入数据,多条数据
            List<OrderDetail> orderDetails = shoppingCarts.stream().map((item) -> {
                OrderDetail orderDetail = new OrderDetail();
                orderDetail.setOrderId(orderId);
                orderDetail.setNumber(item.getNumber());
                orderDetail.setDishFlavor(item.getDishFlavor());
                orderDetail.setDishId(item.getDishId());
                orderDetail.setSetmealId(item.getSetmealId());
                orderDetail.setName(item.getName());
                orderDetail.setImage(item.getImage());
                orderDetail.setAmount(item.getAmount());
                
                //BigDecimal:用于表示高精度的十进制数值,支持浮点数的精确计算。
                //BigInteger:用于表示任意精度的整数,支持整数的精确计算。
                amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
                
                return orderDetail;
            }).collect(Collectors.toList());
    
            orderDetailService.saveBatch(orderDetails);
    
            //向订单表插入数据,一条数据
            orders.setId(orderId);
            orders.setOrderTime(LocalDateTime.now());
            orders.setCheckoutTime(LocalDateTime.now());
            orders.setStatus(2);
            orders.setAmount(new BigDecimal(amount.get()));//总金额
            orders.setUserId(userId);
            orders.setNumber(String.valueOf(orderId));
            orders.setUserName(user.getName());
            orders.setConsignee(addressBook.getConsignee());
            orders.setPhone(addressBook.getPhone());
            orders.setAddress((addressBook.getProvinceName() == null ? "" : addressBook.getProvinceName())
                    + (addressBook.getCityName() == null ? "" : addressBook.getCityName())
                    + (addressBook.getDistrictName() == null ? "" : addressBook.getDistrictName())
                    + (addressBook.getDetail() == null ? "" : addressBook.getDetail()));
    
            //清空购物车数据
            shoppingCartService.remove(wrapper);
        }

 

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值