唐人外卖项目总结

一、项目介绍

唐人外卖项目是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括系统管理后台和移动端应用两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的菜品、套餐、订单等进行管理维护。移动端应用主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单等。

在这里插入图片描述

二、功能描述

1、管理端:餐饮企业内部员工使用。 主要功能有:
在这里插入图片描述

2、移动端:主要提供给消费者使用。主要功能有:
在这里插入图片描述

3、数据库文件
在这里插入图片描述

三、员工管理功能实现

1、登录功能

在这里插入图片描述

  • 1.1 处理逻辑

    1、将页面提交的密码password进行md5加密处理
    2、根据页面提交的用户名username查询数据库
    3、如果没有查询到则返回登录失败结果
    4、密码比对,如果不一致则返回登录失败结果
    5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
    6、登录成功,将员工id存入Session并返回登录成功结果

  • 1.2 处理流程

在这里插入图片描述

  • 1.3 数据流向

    前端发送请求,后端controller响应请求,同时调用service方法,service层又会通过mapper层操作数据库,完成数据的查询。
    在这里插入图片描述

  • 1.4 代码实现

	/**
     * 员工登录
     * @param request   获取员工登录后的id存入session
     * @param employee
     * @return
     */
    @PostMapping("/login")
    public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){       //requestBody接收json数据
        //1.将页面提交的密码进行MD5加密处理
         String password = employee.getPassword();     //获取密码
         password = DigestUtils.md5DigestAsHex(password.getBytes());
        //2.根据用户提交的用户名查询数据库
        LambdaQueryWrapper<Employee> queryWrapper =new LambdaQueryWrapper<>();
        queryWrapper.eq(Employee::getUsername,employee.getUsername());
        //查询用户 ,数据库中的唯一索引 所以使用getOne
         Employee emp = employeeService.getOne(queryWrapper);
        //3.处理查询结果
        if (emp ==null){
            return R.error("登录失败!");
        }
        //4.密码比对,如果不一致则返回登录失败
        if (!emp.getPassword().equals(password)){
            return R.error("密码错误,登录失败!");
        }
        //5.查看员工状态,看是否被禁用,0表示禁用
        if (emp.getStatus()==0){
            return R.error("该账号已禁用!");
        }
        //6.登录成功,将员工id存入session并返回登录结果
         request.getSession().setAttribute("employee",emp.getId());
        return R.success(emp);
    }
  • 1.5 功能测试

    发送请求:
    在这里插入图片描述

    数据封装:
    在这里插入图片描述

2、登录功能完善-拦截器

  • 2.1 创建过滤器LoginCheckFilter
    过滤器具体的处理逻辑如下:
    1、获取本次请求的URI
    2、判断本次请求是否需要处理
    3、如果不需要处理,则直接放行
    4、判断登录状态,如果已登录,则直接放行
    5、如果未登录则返回未登录结果
  • 2.2 处理过程
    在这里插入图片描述
  • 2.3 代码实现
/**
 * 检查用户是否登录
 */
@Slf4j
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")          //拦截路径
public class LoginCheckFilter implements Filter {
    //路径匹配器,支持通配符,Spring提供的工具包
    public static final AntPathMatcher PATH_MATCHER =new AntPathMatcher();

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request =(HttpServletRequest)servletRequest;
        HttpServletResponse response= (HttpServletResponse)servletResponse;
        //1.获取本次请求的URI
        String requestURI=request.getRequestURI();          //backend/index.html

        log.info("拦截到请求:{}",requestURI);
        //定义一些不需要处理的请求路径,直接放行
        String[] urls = new String[]{
                "/employee/login",
                "employee/logout",
                "/backend/**",
                "/front/**",
                "/common/**",
                "/user/sendMsg",                //移动端发送短信
                "/user/login"                       //移动端登录
        };
        //2.判断本次请求是否需要处理
        boolean check = check(urls, requestURI);

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

            Long empId =(Long) request.getSession().getAttribute("employee");
            //将获取到的用户id存入当前线程
            BaseContext.setCurrentId(empId);

            filterChain.doFilter(request,response);
            return;
        }
        //4-2.判断移动端登录状态,如果已经登录,则直接放行
        if (request.getSession().getAttribute("user") !=null){
            log.info("用户已登陆,用户ID为{}",request.getSession().getAttribute("user"));

            Long userId =(Long) request.getSession().getAttribute("user");
            //将获取到的用户id存入当前线程
            BaseContext.setCurrentId(userId);

            filterChain.doFilter(request,response);
            return;
        }
        log.info("用户未登录");
        //5.未登录,则返回未登录结果,通过输出流的方式,向客户端页面响应数据
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;
    }
    /**
     * 路径匹配,检查本次请求是否需要放行
     * @param urls
     * @param requestURI
     * @return
     */
    public boolean check(String[] urls,String requestURI){
        for (String url :urls){
             boolean match = PATH_MATCHER.match(url, requestURI);
            if (match){
                //路径匹配成功
                return true;
            }
        }
        return false;
    }
}

3、新增员工
在这里插入图片描述

  • 3.1 处理逻辑:
    1、页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
    2、服务端Controller接收页面提交的数据并调用Service
    3、Service调用Mapper操作数据库,保存数据
    在这里插入图片描述
  • 3.2 数据流向
    在这里插入图片描述
  • 3.3 逻辑代码
    /**
     * 新增员工
     * @param employee
     * @return
     */
    @PostMapping
    public R<String> save(HttpServletRequest request, @RequestBody Employee employee){
        log.info("员工信息:{}",employee.toString());
        //设置用户的初始密码 12345,需要进行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);
        //mp提供的方法
         employeeService.save(employee);
         return R.success("新增员工成功");
    }

4、员工信息分页查询

  • 4.1 处理逻辑
    1、页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
    2、服务端Controller接收页面提交的数据并调用Service查询数据
    3、Service调用Mapper操作数据库,查询分页数据
    4、Controller将查询到的分页数据响应给页面
    在这里插入图片描述
  • 4.2 逻辑代码
/**
     * 员工信息的分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")
    public  R<Page> page(int page,int pageSize,String name){        //mp提供的Page对象,包含相关的属性
        log.info("page ={},pageSize={},name={}",page,pageSize,name);
        //构造分页构造器
        Page pageInfo =new Page(page,pageSize);
        //条件构造器
        LambdaQueryWrapper<Employee> queryWrapper =new LambdaQueryWrapper();
        //添加过滤条件
        queryWrapper.like(StringUtils.hasText(name),Employee::getName,name);
      //添加排序条件,按更新时间排序
        queryWrapper.orderByDesc(Employee::getUpdateTime);
        //执行查询
        employeeService.page(pageInfo,queryWrapper);
        return R.success(pageInfo);
    }

  • 4.3 功能测试
    新增员工信息时完成员工信息的封装在这里插入图片描述

5、员工状态修改

  • 5.1 处理逻辑
    1、页面发送ajax请求,将参数(id、status)提交到服务端
    2、服务端Controller接收页面提交的数据并调用Service更新数据
    3、Service调用Mapper操作数据库
    在这里插入图片描述
  • 5.2 逻辑代码
    /**
     * 根据id修改员工信息
     * @param employee
     * @return
     */
    @PutMapping
    public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
        log.info(employee.toString());
        //调用业务层方法(MP提供的)修改员工信息
        employeeService.updateById(employee);
        return R.success("员工信息修改成功!");
    }
  • 5.3 功能测试
    测试过程中没有报错,但是功能并没有实现,查看数据库中的数据也没有变化。
    观察控制台输出的SQL:
    在这里插入图片描述

Bug根源:前端提供的用户id在执行SQL语句中的id和数据库保存的用户id不一致, 导致前后端获得的用户id不一致,所以数据更新失败

原因:js对long型数据进行处理时丢失精度,导致提交的id和数据库中的id不一致
解决办法:在服务端给页面响应json数据时,将long型数据统一转为String字符串 具体实现:
使用消息对象映射器JacksonObjectMapper将java对象转换为json数据:
在WebMvcConfig配置类中扩展SpringMvc的消息转换器,在此消息转换器中使用提供的对象转换器进行java对象到json数据的转换,将前端所有相关数据转换为指定类型:

 /**
     * 扩展SpringMvc的消息转换器
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器......");
        //创建消息转换器对象,将返回结果转成相应的json,通过输出流的方式响应给页面
        MappingJackson2HttpMessageConverter messageConverter =new MappingJackson2HttpMessageConverter();
        //设置对象转换器,底层使用Jackson将java对象转为json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        //将上面的消息转换器对象追加到mvc框架的转换器集合中
        converters.add(0,messageConverter);
}

在这里插入图片描述

6、修改员工信息

  • 6.1 处理逻辑
    1、点击编辑,发送ajax请求,请求服务端,同时提交员工id参数
    2、服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
    3、页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
    4、点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端
    5、服务端接收员工信息,并进行处理,完成后给页面响应
    在这里插入图片描述
  • 6.2 逻辑代码
 /**
     * 根据id修改员工信息
     * @param employee
     * @return
     */
    @PutMapping
    public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
        log.info(employee.toString());
        employeeService.updateById(employee);
        return R.success("员工信息修改成功!");
    }

四、分类管理功能实现

1、公共字段自动填充
数据库中多张表存在创建时间、更新时间、更新人等字段,这些字段在进行数据的插入、更新操作时需要频繁的手动添加,因此,使用Mybatis-plus提供的公共字段填充,来简化开发,开发步骤如下:
(1)添加注解,指定属性为公共字段,括号后面表示的是填充策略
在这里插入图片描述

在这里插入图片描述
(2)添加元数据对象处理器MyMetaObjectHandler实现MetaObjectHandler接口

/**
 * 自定义元数据对象处理器
 */
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
    /**
     * 插入操作自动填充
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共字段自动填充[insert]");
        log.info(metaObject.toString());
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("createUser", BaseContext.getCurrentId());
        metaObject.setValue("updateUser", BaseContext.getCurrentId());
    }
    /**
     * 更新操作自动填充
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("公共字段自动填充[update]");
        log.info(metaObject.toString());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("updateUser", BaseContext.getCurrentId());
    }
}

2、优化字段填充
客户端每次发送的请求,对应的在服务端都会分配一个新的线程来处理。这里使用Threadlocal进行优化:

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

Threadlocal常用的方法:
Public void set(T value); //设置当前线程的局部变量的值
Public T get(); //返回当前线程局部变量的值

以下三个方法都属于同一线程:
1、LoginCheckFilter的doFilter方法
2、EmployeeController的update方法
3、MyMetaObjectHandler的updateFill方法

优化步骤:

1.编写BaseContext工具类,基于ThreadLocal封装的工具类
2.在LoginCheckFilter的doFilter方法中获取当前用户的id,调用Threadlocal的set方法设置当前线程局部变量的值;
3.在MyMetaObjectHandler的updateFill方法中调用Threadlocal的get方法获取当前线程局部变量的值;

/**
 * 基于ThreadLocal的封装工具类,用于保存和获取当前登录用户的id
 */
public class BaseContext {
    private static ThreadLocal<Long> threadLocal =new ThreadLocal<>();
    //设置当前线程的id
    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }
    //获取当前线程id
    public static Long getCurrentId(){
        return threadLocal.get();
    }
}

3、新增分类

在这里插入图片描述

  • 3.1 处理逻辑
    1、页面(backend/page/category/list.html)发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端
    2、服务端Controller接收页面提交的数据并调用Service将数据进行保存
    3、Service调用Mapper操作数据库,保存数据
    菜品和套餐除类型不一样,提交json数据结构都相同,可共用一个方法统一处理:
    在这里插入图片描述
  • 3.2 逻辑代码
    /**
     * 新增分类
     * @param category
     * @return
     */
    @PostMapping
    public R<String> save(@RequestBody Category category){
        log.info("category:{}",category);
        categoryService.save(category);
        return  R.success("新增分类成功!");
    }

4、分类信息分页查询
在这里插入图片描述

  • 4.1 处理逻辑
    1、页面发送ajax请求,将分页查询参数(page、pageSize)提交到服务端
    2、服务端Controller接收页面提交的数据并调用Service查询数据
    2.Service调用Mapper操作数据库,查询分页数据
    3.Controller将查询到的分页数据响应给页面
    4.页面作数据展示
    在这里插入图片描述
  • 4.2 逻辑代码
/**
     * 分页查询
     * @param page
     * @param pageSize
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(int page,int pageSize){
        //分页构造器
        Page<Category> pageInfo =new Page<>(page,pageSize);
        //条件构造器
        LambdaQueryWrapper<Category> queryWrapper =new LambdaQueryWrapper<>();
        //添加排序条件,根据sort字段排序
        queryWrapper.orderByAsc(Category::getSort);

        //分页查询
        categoryService.page(pageInfo,queryWrapper);
        return R.success(pageInfo);
    }

5、删除分类

  • 5.1 处理逻辑
    1、页面发送ajax请求,将参数(id)提交到服务端
    2、服务端Controller接收页面提交的数据并调用Service删除数据
    3、Service调用Mapper操作数据库
    在这里插入图片描述
  • 5.2 代码实现
    /**
     * 根据id删除分类
     * @param ids
     * @return
     */
    @DeleteMapping
    public R<String> delete(Long ids){
        log.info("删除菜品,id为:{}",ids);
        categoryService.remove(ids);
        return R.success("分类信息删除成功!");
    }
  • 5.3 功能完善
    检查删除的分类是否关联了菜品或者套餐,如果关联了则无法删除,要完善分类删除功能,需要先准备基础的类和接口:
    1、实体类Dish和Setmeal
    2、Mapper接口:DishMapper和SetmealMapper
    3、Service接口:DishService和SetmealService
    4、Service实现类:Dishservicelmpl和SetmealServicelmpl

自定义删除方法:

    /**
     * 根据id删除分类,删除之前先判断是否有关联
     * @param id
     */
    @Override
    public void remove(Long id) {
        LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
        //根据分类id进行查询
        dishLambdaQueryWrapper.eq(Dish::getCategoryId, id);
        int count1 = dishService.count(dishLambdaQueryWrapper);
        //查询当前分类是否关联了菜品,如果已关联了,抛出业务异常
        if (count1 > 0) {
            //已关联菜品,抛出业务异常
            throw new CustomException("当前分类下关联了菜品,不能删除");
        }
        //查询当前分类是否关联了套餐,如果已关联,抛出业务异常
        LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
        //根据id进行查询
        setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId, id);
        int count2 = setmealService.count();
        if (count2 > 0) {
            //已关联了套餐
            throw new CustomException("当前分类下关联了套餐,不能删除");
        }
        //正常情况下,正常删除
        super.removeById(id);
    }

若当前分类下关联了菜品或者套餐,则无法删除,添加一个业务异常类:

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

在全局异常处理器中添加异常捕获:

    /**
     * 处理自定义异常
     * @param ex
     * @return
     */
    @ExceptionHandler(CustomException.class)
    public R<String> exceptionHandler(CustomException ex) {      //捕获到的异常
        log.error(ex.getMessage());
        return R.error(ex.getMessage());
    }

五、菜品管理功能实现

1、文件的上传和下载
注意:文件上传时,对页面的form表单有如下要求:

method="post"							采用post方式提交数据
enctype="multipart/form-data"			采用multipart格式上传文件		
type="file"							    使用input的file控件上传

Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件,例如:
在这里插入图片描述

  • 1.1 处理逻辑
    1.获取上传文件的原始文件名;
    2.使用UUID重新生成文件名,防止文件名重复造成文件覆盖;
    3.创建一个目录对象,文件转存到指定位置
    4.文件上传完毕后返回文件名称,将文件存入数据库
  • 1.2 逻辑代码
 	/**
     * 文件的上传
     * @param file
     * @return
     */
    @PostMapping("/upload")
    public R<String> update(MultipartFile file) {       //file参数名已经和前端关联,此处文件名必须是file
        log.info(file.toString());
        //获取上传文件的原始文件名
        String originalFilename = file.getOriginalFilename();
        //suffix 为文件的后缀名
         String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
        //使用UUID重新生成文件名,防止文件名重复造成文件覆盖
        String fileName = UUID.randomUUID().toString()+suffix;         //32位文件名

        //创建一个目录对象
        File dir =new File(basePath);
        //判断目录是否存在
        if (!dir.exists()){
            //不存在目录,则需要创建
            dir.mkdirs();
        }
        try {
            //文件转存到指定位置
            file.transferTo(new File(basePath+fileName));
        } catch (IOException e) {
            e.printStackTrace();
        }
        //文件上传完毕后返回文件名称,将文件存入数据库
        return R.success(fileName);
    }
  • 1.3 文件下载
    浏览器回显上传图片展示的过程,其实就是一次图片的下载,文件的上传下载本质上是对流的操作,通过IO操作从输入流中读取文件,再将图片信息展示到浏览器
  • 1.4 逻辑代码
  	/**
     * 文件下载
     * @param name
     */
    @GetMapping("/download")
    public void download(String name, HttpServletResponse response){
        try {
            //1.通过输入流读取文件内容
            FileInputStream fileInputStream =new FileInputStream(new File(basePath+name));
            //2.通过输出流将文件写回浏览器,在浏览器展示图片
            //在这里需要通过响应对象获取输出流
            ServletOutputStream outputStream = response.getOutputStream();

            //设置响应回去的文件类型
            response.setContentType("img/jpeg");

            byte[] bytes =new byte[1024];
            int len =0;
            //一直读取文件
            while ((len =fileInputStream.read(bytes)) !=-1){
               //将读取的文件写入
                outputStream.write(bytes,0,len);
                //刷新缓冲区
                outputStream.flush();
            }
            //3.关闭资源
            outputStream.close();
            fileInputStream.close();
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

2、新增菜品

新增的菜品会同步在移动端展示:
在这里插入图片描述

  • 2.1 处理逻辑
    1.页面发送ajax请求回显数据;
    2.页面发送请求进行图片的上传;
    3.页面发送请求进行图片的下载;
    4.点击保存按钮,发送ajax请求,将菜品的数据已json形式提交到服务端。
    Dish表中不存在口味信息,因此在数据操作时,新定义dishDto类,可以封装所有json数据提交到前端页面
    在这里插入图片描述
  • 2.2 逻辑代码
    新增菜品设计到多张表的操作,操作过程较为复杂:
 /**
     * 新增菜品同时保存口味数据
     * @param dishDto
     */
    @Override
    @Transactional      //对多张表操作需要开启事务,同时需要再启动类加注解配置
    public void saveWithFlavor(DishDto dishDto) {
        //保存菜品的基本信息到dish表
        this.save(dishDto);
         Long dishId = dishDto.getId();     //菜品id

        //获得菜品口味的集合
         List<DishFlavor> flavors = dishDto.getFlavors();
        //stream流的方式遍历集合,lambda表达式,为集合中的每个flavors赋值菜品id
        flavors=flavors.stream().map((item) ->{
           item.setDishId(dishId);
           return item;
        }).collect(Collectors.toList());            //相当于对集合做处理后又重新转为list集合

        //保存菜品的口味表到菜品口味表dish_flavor
     dishFlavorService.saveBatch(flavors);          //saveBatch保存集合数据
    }

3、菜品信息分页查询

在这里插入图片描述

  • 3.1 处理逻辑
    1.先查询菜品的基本信息 --dish表
    2.创建一个Dto拷贝普通的属性
    3.查询当期菜品所对应的口味信息 --dish_flavor表
    4.最后为dto设置flavors属性
  • 3.2 逻辑代码
    /**
     * 根据id查询菜品信息和口味信息
     * @param id
     * @return
     */
    @Override
    public DishDto getByIdWithFlavor(Long id) {
        //1.先查询菜品的基本信息 --dish表
         Dish dish = this.getById(id);

         //创建一个Dto拷贝普通的属性
        DishDto dishDto =new DishDto();
        BeanUtils.copyProperties(dish,dishDto);

        //2.查询当期菜品所对应的口味信息      --dish_flavor表
        //条件构造器
        LambdaQueryWrapper<DishFlavor> queryWrapper =new LambdaQueryWrapper<>();
        //构造条件
        queryWrapper.eq(DishFlavor::getDishId,dish.getId());
        List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);

        //最后为dto设置flavors属性
        dishDto.setFlavors(flavors);

        return dishDto;
    }

4、修改菜品

在这里插入图片描述

  • 4.1 处理逻辑
    1、页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示
    2、页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
    3、页面发送请求,请求服务端进行图片下载,用于页图片回显
    4、点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端
  • 4.2 逻辑代码
	/**
     * 更新菜品信息,同时更新对应的口味信息
     * @param id
     * @return
     */
    @Override
    @Transactional
    public void updateWithFlavor(DishDto dishDto) {
        //1.更新菜品dish信息
        this.updateById(dishDto);
        //2.更新dish_flavors口味信息
        //先删除当前菜品对应的口味信息---delete
        LambdaQueryWrapper<DishFlavor> queryWrapper =new LambdaQueryWrapper();
        queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());

        dishFlavorService.remove(queryWrapper);
        //再添加当前提交的口味信息---insert
         List<DishFlavor> flavors = dishDto.getFlavors();           //获取当前表单中的口味信息

        flavors =flavors.stream().map((item) ->{
            item.setDishId(dishDto.getId());
            return item;
        }).collect(Collectors.toList());

        //将集合数据存入数据库
        dishFlavorService.saveBatch(flavors);
    }

六、套餐管理功能实现

1、新增套餐
新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal_dish表插入套餐和菜品关联数据。所以在新增套餐时,涉及到两个表:

  • setmeal 套餐表

  • setmeal_dish 套餐菜品关系表

  • 处理逻辑
    1、页面(backend/page/combo/add.html)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中
    2、页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
    3、页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
    4、页面发送请求进行图片上传,请求服务端将图片保存到服务器
    5、页面发送请求进行图片下载,将上传的图片进行回显
    6、点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端

  • 代码实现

 /**
     * 新增套餐,同时保存套餐和菜品的关联关系
     * @param setmealDto
     */
    @Override
    @Transactional
    public void saveWithDish(SetmealDto setmealDto) {
        //保存套餐的基本信息,操作Setmeal,执行insert操作
        this.save(setmealDto);

        final List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
        //为集合上的每个对象赋予setmealId
        setmealDishes.stream().map((item) ->{
            item.setSetmealId(setmealDto.getId());
            return item;
        }).collect(Collectors.toList());

        //保存套餐和菜品的关联关系,操作Setmeal_dish,执行insert操作
        setmealDishService.saveBatch(setmealDishes);
}

2、删除套餐

  • 2.1 处理逻辑
  • 删除单个套餐时,页面发送ajax请求,根据套餐id删除对应套餐

在这里插入图片描述

  • 删除多个套餐时,页面发送ajax请求,根据提交的多个套餐id删除对应套餐

在这里插入图片描述

  • 2.2 代码实现
 /**
     * 删除套餐,同时删除套餐和菜品的关联数据
     * @param ids
     */
    @Override
    @Transactional
    public void removeWithDish(List<Long> ids) {
        //查询套餐的状态,仅起售状态的可以删除
        //查询条件 select count(*) from setmetal 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);
        //删除关系表中的数据--- setmeal_dish
        // delete from  setmetal_dish where setmeal_id in ();
        //构造查询条件
        LambdaQueryWrapper<SetmealDish> dishQueryWrapper =new LambdaQueryWrapper<>();
        dishQueryWrapper.in(SetmealDish::getSetmealId,ids);

        setmealDishService.remove(dishQueryWrapper);
}

3、修改套餐的售卖状态

  • 3.1 处理逻辑
    1.根据前端传入的id查询套餐集合;
    2.查询条件按照套餐价格降序;
    3.遍历套餐集合,更新集合中各个套餐的状态;

  • 3.2 代码实现

    /**
     * 修改套餐的售卖状态
     *
     * @return
     */
    @PostMapping("/status/{status}")
    public R<String> statusWithIds(@PathVariable("status") Integer status, @RequestParam List<Long> ids) {
        //构造一个条件构造器
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.in(ids != null, Setmeal::getId, ids);
        queryWrapper.orderByDesc(Setmeal::getPrice);
        //根据条件进行批量查询
        List<Setmeal> list = setmealService.list(queryWrapper);
        for (Setmeal setmeal : list) {
            if (list != null) {
                //把浏览器传入的status参数复制给套餐
                setmeal.setStatus(status);
                setmealService.updateById(setmeal);
            }
        }
        return R.success("售卖状态修改成功");
    }

4、移动端套餐查询

  • 处理逻辑
    1.根据套餐id查询套餐;
    2.添加查询条件:套餐未被禁用状态下,按照更新时间的降序排列;
    3.返回查询到的套餐列表;

  • 代码实现

 /**
     * 移动端
     * 套餐查询
     * @param setmeal
     * @return
     */
    @GetMapping("/list")
    public R<List<Setmeal>> list(Setmeal setmeal){
        //构造查询条件
        LambdaQueryWrapper<Setmeal> queryWrapper =new LambdaQueryWrapper<>();
        //菜品id
        queryWrapper.eq(setmeal.getCategoryId()!= null,Setmeal::getCategoryId,setmeal.getCategoryId());
        //售卖状态
        queryWrapper.eq(setmeal.getStatus()!= null,Setmeal::getStatus,setmeal.getStatus());
        //添加一个排序条件
        queryWrapper.orderByDesc(Setmeal::getUpdateTime);

         List<Setmeal> list = setmealService.list(queryWrapper);
        return R.success(list);
    }

七、移动端功能实现

1、用户登录

在这里插入图片描述

  • 1.1 处理逻辑
    1、在登录页面(front/page/login.html)输入手机号,点击【获取验证码】按钮,页面发送aiax请求,在服务端调用短信服务API给指定手机号发送验证码短信
    2、在登录页面输入验证码,点击【登录】按钮,发送ajax请求,在服务端处理登录请求
    3、在过滤器中添加登录的处理请求
    在这里插入图片描述
    4、从session中获取验证码进行比对
    5、根据手机号从数据库查询用户信息,如果用户不存在,就直接注册新用户
    6、登录成功,将用户信息存入session
  • 1.2 代码实现
    /**
     * 移动端登录
     * @param map
     * @param session
     * @return
     */
    @PostMapping("/login")
    public R<User> login(@RequestBody Map map, HttpSession session) {

        log.info(map.toString());
        //获取手机号
         String phone = map.get("phone").toString();
        //获取验证码
         String code = map.get("code").toString();
        //从session中获取保存的验证码
         Object codeInSession = session.getAttribute(phone);
        //页面提交的验证码和session中保存的验证码比对
        if (codeInSession !=null && codeInSession.equals(code)){
            //比对成功,说明登录成功
            LambdaQueryWrapper<User> queryWrapper =new LambdaQueryWrapper<>();
            //查询数据库中的手机号
            queryWrapper.eq(User::getPhone,phone);

             User user = userService.getOne(queryWrapper);
            if (user ==null){
                //判断当前手机号是否为新用户,如果是新用户就自动完成注册
                user =new User();
                user.setPhone(phone);
                user.setStatus(1);
                //完成注册
                userService.save(user);
            }
            //登录成功后,将userID存入session中
            session.setAttribute("user",user.getId());
            return  R.success(user);
        }
        return R.error("登录失败");
    }

2、菜品展示

在这里插入图片描述

  • 逻辑功能
    1、页面(front/index.html)发送ajax请求,获取分类数据(菜品分类和套餐分类)
    2、页面发送ajax请求,获取第一个分类下的菜品或者套餐
    开发菜品展示功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
  • 代码实现
  /**
     * 通过id查询套餐信息, 同时还要查询关联表setmeal_dish的菜品信息进行回显
     * @param id 待查询的id
     */
    @Override
    public SetmealDto getByIdWithDish(Long id) {
        // 根据id查询setmeal表中的基本信息
        Setmeal setmeal = this.getById(id);
        SetmealDto setmealDto = new SetmealDto();
        // 对象拷贝。
        BeanUtils.copyProperties(setmeal, setmealDto);
        // 查询关联表setmeal_dish的菜品信息
        LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SetmealDish::getSetmealId, id);
        List<SetmealDish> setmealDishList = setmealDishService.list(queryWrapper);
        //设置套餐菜品属性
        setmealDto.setSetmealDishes(setmealDishList);
        return setmealDto;
    }

3、添加购物车

在这里插入图片描述

  • 处理逻辑

1、点击加入购物车或者按钮,页面发送ajax请求,请求服务端,将菜品或者套餐添加到购物车
2、点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐
3、点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作

  • 代码实现
 /**
     * 添加购物车
     * @param shoppingCart
     * @return
     */
    @PostMapping("/add")
    public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
        log.info("购物车数据:{}",shoppingCart);

        //1.设置用户id,指定是哪个用户的购物车
        Long currentId = BaseContext.getCurrentId();
        shoppingCart.setUserId(currentId);

        //2.查询当前菜品或套餐是否已经存在于购物车中
        final Long dishId = shoppingCart.getDishId();

        LambdaQueryWrapper<ShoppingCart> queryWrapper =new LambdaQueryWrapper<>();
        //根据用户id查询
        queryWrapper.eq(ShoppingCart::getUserId,currentId);

        if (dishId != null){
            //添加菜品信息到购物车
            queryWrapper.eq(ShoppingCart::getDishId,dishId);
        }else {
            //添加套餐到购物车
            queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
        }
        //SQL:select * from shopping_cart where user_id =? and dish_id/setmeal_id =?
         ShoppingCart cartOne = shoppingCartService.getOne(queryWrapper);

        if (cartOne != null){
            log.info("cartOne ==null!");
            //已经存在,就在原来的基础上+1
            Integer number = cartOne.getNumber();                //原来的数量
            cartOne.setNumber(number+1);
            shoppingCartService.updateById(cartOne);            //更新操作
        }else {
            //不存在,添加到购物车,数量默认1
            shoppingCart.setNumber(1);
            shoppingCart.setCreateTime(LocalDateTime.now());
            shoppingCartService.save(shoppingCart);
            //重新把shoppingCart赋给cartOne
            cartOne =shoppingCart;
        }
        return R.success(cartOne);
    }

4、用户下单

在这里插入图片描述

  • 处理逻辑

1、在购物车中点击去结算按钮,页面跳转到订单确认页面
2、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的默认地址
3、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的购物车数据
4、在订单确认页面点击支付按钮,发送ajax请求,请求服务端完成下单操作

  • 代码实现
/**
     * 用户下单
     * @param orders
     */
    @Override
    @Transactional
    public void submit(Orders orders) {
        //先获得用户id
        final Long currentId = BaseContext.getCurrentId();

        //查询当前用户的购物车数据
        LambdaQueryWrapper<ShoppingCart> queryWrapper =new LambdaQueryWrapper<>();
        queryWrapper.eq(ShoppingCart::getUserId,currentId);
        //查询到购物车数据
        final List<ShoppingCart> shoppingCarts = shoppingCartService.list(queryWrapper);

        //对购物车数据进行判断
        if (shoppingCarts ==null || shoppingCarts.size()==0){
            throw  new CustomException("购物车为空,不能下单");
        }
        //查询用户信息
        final User user = userService.getById(currentId);

         Long addressBookId = orders.getAddressBookId();
         AddressBook addressBook = addressBookService.getById(addressBookId);

         if (addressBook ==null){
             throw new CustomException("地址信息为空,不能下单");
         }
        //向订单表中添加数据
         long orderId = IdWorker.getId();       //mp提供的方法生成订单号

        //原子操作
        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());
            amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
            return orderDetail;
        }).collect(Collectors.toList());

        //设置订单实体的其他属性
        orders.setId(orderId);
        orders.setOrderTime(LocalDateTime.now());
        orders.setCheckoutTime(LocalDateTime.now());
        orders.setStatus(2);        //待派送
        orders.setAmount(new BigDecimal(amount.get()));     //总金额
        orders.setUserId(currentId);        //用户id
        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()));
        this.save(orders);
        //向订单明细表插入数据,多条数据
        orderDetailService.saveBatch(orderDetails);

        //下单完成后,清空购物车数据
        shoppingCartService.remove(queryWrapper);
    }

6、后台查询订单明细

  • 处理逻辑
    1、根据分页信息查询订单;
    2、添加查询条件,根据更新时间降序排列;
    3、对于查询到的订单信息,进行信息处理用于前端页面展示;
  • 代码实现
 /**
     * 后台查询订单明细
     * @param page
     * @param pageSize
     * @param number
     * @param beginTime
     * @param endTime
     * @return
     */
    @Override
    public Page<OrdersDto> empPage(int page, int pageSize, String number, String beginTime, String endTime) {
        Page<Orders> pageInfo = new Page<>(page, pageSize);
        Page<OrdersDto> pageDto = new Page<>();

        //创建条件构造器
        LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
        //添加条件,根据number进行like模糊查询
        queryWrapper.like(number != null, Orders::getNumber, number);
        queryWrapper.gt(StringUtils.isNotEmpty(beginTime), Orders::getOrderTime, beginTime);
        queryWrapper.lt(StringUtils.isNotEmpty(endTime), Orders::getOrderTime, endTime);
        //添加排序条件(根据更新时间降序排序)
        queryWrapper.orderByDesc(Orders::getOrderTime);
         this.page(pageInfo, queryWrapper);
        //将其除了records中的内存复制到pageDto中
        BeanUtils.copyProperties(pageInfo, pageDto, "records");

        List<Orders> records = pageInfo.getRecords();
        List<OrdersDto> collect = records.stream().map((item) -> {
            OrdersDto ordersDto = new OrdersDto();
            //对象拷贝
            BeanUtils.copyProperties(item, ordersDto);
            LambdaQueryWrapper<OrderDetail> lambdaQueryWrapper = new LambdaQueryWrapper<>();
            //根据订单id查询订单详细信息
            lambdaQueryWrapper.eq(OrderDetail::getOrderId, item.getId());

            List<OrderDetail> orderDetails = orderDetailService.list(lambdaQueryWrapper);
            ordersDto.setOrderDetails(orderDetails);

            //根据userId查询用户姓名
            Long userID = item.getUserId();
            User user = userService.getById(userID);
            ordersDto.setUserName(user.getName());
            ordersDto.setPhone(user.getPhone());

            //获取地址信息
            Long addressBookId = item.getAddressBookId();
            AddressBook addressBook = addressBookService.getById(addressBookId);
            ordersDto.setAddress(addressBook.getDetail());
            ordersDto.setConsignee(addressBook.getConsignee());

            return ordersDto;
        }).collect(Collectors.toList());

        pageDto.setRecords(collect);
        return pageDto;
    }

项目进阶优化篇博客:唐人外卖——项目优化篇

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值