瑞吉外卖项目----(1)基本功能实现

目录

  • 0 软件开发整体介绍
  • 1 项目整体介绍
    • 1.1 项目介绍
    • 1.2 产品原型
    • 1.3 技术选型
    • 1.4 功能架构
    • 1.5 角色
  • 2 开发环境搭建
    • 2.1 数据库环境搭建
    • 2.2 Maven项目搭建
    • 2.3 启动测试
    • 2.4 导入前端页面
  • 3 后台登录登出功能
    • 3.1 登录功能
      • 3.1.1 需求分析
      • 3.1.2 代码实现
    • 3.2 退出功能
      • 3.2.1 需求分析
      • 3.2.2 代码实现
    • 3.3 拦截登录页面
  • 4 员工管理
    • 4.1 新增员工
      • 4.1.1 需求分析
      • 4.1.2 数据模型
      • 4.1.3 代码实现
      • 4.1.4 异常拦截处理
    • 4.2 员工信息分页查询
      • 4.2.1 需求分析
    • 4.2.2 代码实现
    • 4.3 启用/禁用员工账号---js精度丢失问题
      • 4.3.1 需求分析
      • 4.3.2 代码实现
    • 4.4 编辑员工信息
      • 4.4.1 需求分析
      • 4.4.2 代码开发
  • 5 分类操作
    • 5.1 公共字段自动填充
      • 5.1.1 问题分析
      • 5.1.2 代码实现
      • 5.1.3 功能完善
    • 5.2 新增分类
      • 5.2.1 需求分析
      • 5.2.2 数据模型
      • 8.2.3 代码开发
    • 5.3 分类信息分页查询
      • 5.3.1 需求分析
      • 5.3.2 代码开发
    • 5.4 删除分类
      • 5.4.1 需求分析
      • 5.4.2 基础删除代码实现
      • 5.4.3 功能完善----检查是否有关联再删除
    • 5.5 修改分类
      • 5.5.1 需求分析
      • 8.5.2 代码实现
  • 6 菜品管理业务开发
    • 6.1 文件的上传下载
      • 6.1.1 文件上传下载介绍
      • 6.1.2 文件上传代码实现
      • 6.1.3 文件下载代码实现
    • 6.2 新增菜品
      • 6.2.1 需求分析
      • 6.2.2 数据模型
      • 6.2.3 代码开发
    • 6.3 菜品信息分页查询
      • 6.3.1 需求分析
      • 6.3.2 代码实现
    • 6.4 修改菜品
      • 6.4.1 需求分析
      • 6.4.2 代码实现
  • 7 套餐管理业务开发
    • 7.1 新增套餐
      • 7.1.1 需求分析
      • 7.1.2 数据模型
      • 7.1.3 代码开发
    • 7.2 套餐信息分页查询
      • 7.2.1 需求分析
      • 7.2.2 代码实现
    • 7.3 删除套餐
      • 7.3.1 需求分析
      • 7.3.2 代码实现
  • 8 手机验证码登录
    • 8.1 短信发送
      • 8.1.1 短信服务介绍
      • 8.1.2 腾讯云短信服务
      • 8.1.3 代码开发
    • 8.2 手机验证码
      • 8.2.1 需求分析
      • 8.2.2 数据模型
      • 8.2.3 代码实现
  • 9 菜品展示、购物车、下单
    • 9.1 用户地址薄
      • 9.1.1 需求分析
      • 9.1.2 数据模型
      • 9.1.2 代码实现
    • 9.2 菜品展示
      • 9.2.1 需求分析
      • 9.2.2 代码实现
    • 9.3 购物车
      • 9.3.1 需求分析
      • 9.3.2 数据模型
      • 9.3.3 代码开发
    • 9.4 用户下单
      • 9.4.1 需求分析
      • 9.4.1 数据模型
      • 9.4.1 代码开发

0 软件开发整体介绍

  1. 软件开发流程
    瑞吉外卖项目----(1)基本功能实现_第1张图片

  2. 角色分工
    瑞吉外卖项目----(1)基本功能实现_第2张图片

  3. 软件环境
    在这里插入图片描述

1 项目整体介绍

1.1 项目介绍

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

本项目共分为3期进行开发:
第一期主要实现基本需求,其中移动端应用通过H5实现,用户可以通过手机浏览器访问;
第二期主要针对移动端应用进行改进,使用微信小程序实现,用户使用起来更加方便;
第三期主要针对系统进行优化升级,提高系统的访问性能。

1.2 产品原型

产品原型,就是一款产品成型之前的一个简单的框架,就是将页面的排版布局展现出来,使产品的初步构思有一个可视化的展示。通过原型展示,可以更加直观的了解项目的需求和提供的功能。
产品原型主要用于展示项目的功能,并不是最终的页面效果。
瑞吉外卖项目----(1)基本功能实现_第3张图片瑞吉外卖项目----(1)基本功能实现_第4张图片

1.3 技术选型

瑞吉外卖项目----(1)基本功能实现_第5张图片

1.4 功能架构

瑞吉外卖项目----(1)基本功能实现_第6张图片
瑞吉外卖项目----(1)基本功能实现_第7张图片

1.5 角色

  • 后台系统管理员:登录后台管理系统,拥有后台系统中的所有操作权限
  • 后台系统普通员工:登录后台管理系统,对菜品、套餐、订单等进行管理
  • C端用户:登录移动端应用,可以浏览菜品、添加购物车、设置地址、在线下单等

2 开发环境搭建

2.1 数据库环境搭建

  • 创建一个名为reggie的数据库(图形界面或者命令行都可以)
    报错:unknown variable ‘log-output=FILE‘解决
    在这里插入图片描述
    瑞吉外卖项目----(1)基本功能实现_第8张图片

  • 导入表结构(资料/数据模型/db_reggie.sql)
    在这里插入图片描述
    瑞吉外卖项目----(1)基本功能实现_第9张图片

导入表结构,既可以使用图形界面也可以使用MySQL命令,通过命令行导入表结构时,sql文件不能放在中文目录下

  • 数据表
    瑞吉外卖项目----(1)基本功能实现_第10张图片

2.2 Maven项目搭建

  1. 创建SpringBoot项目

创建完项目后,注意检查项目的编码、maven仓库配置、jdk配置等

  1. 导入pom.xml文件(资料/项目配置文件/pom.xml)
  2. 导入SpringBoot配置文件application.yml(资料/项目配置文件/application.yml)
server:
  port: 8080
spring:
  application:
    name: reggie_take_out
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
    username: root
    password: 123456
mybatis-plus:
  configuration:
    #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: ASSIGN_ID

2.3 启动测试

@Slf4j注解是lombok依赖中的注解,可以设置日志
瑞吉外卖项目----(1)基本功能实现_第11张图片

2.4 导入前端页面

将资料/前端资源目录下的两个文件夹拷贝到resource/static目录下
在这里插入图片描述

扩展:
也可以导入到resources中,就需要放行静态页面,设置资源映射
在默认页面和前台页面的情况下,直接把这俩拖到resource目录下直接访问是访问不到的,因为被mvc框架拦截了 所以我们要编写一个映射类放行这些资源
瑞吉外卖项目----(1)基本功能实现_第12张图片

访问:http://localhost:8080/backend/index.html成功

3 后台登录登出功能

3.1 登录功能

3.1.1 需求分析

登录页面:(页面位置:项目/resources/backend/page/login/login.html)
瑞吉外卖项目----(1)基本功能实现_第13张图片
查看登录请求信息,通过浏览器调试工具(F12),点击登录按钮时,页面会发送请求(请求地址:http://localhost:8080/employee/login)并提交参数(username和password)
瑞吉外卖项目----(1)基本功能实现_第14张图片
数据模型employee表
瑞吉外卖项目----(1)基本功能实现_第15张图片

3.1.2 代码实现

  1. 创建实体类Employee,和数据库表employee表进行映射
@Data
 
//实体类实现Serializable接口,把对象转换为字节序列。序列化操作用于存储时,一般是对于NoSql数据库。
public class Employee implements Serializable {
//在反序列化的过程中,如果接收方为对象加载了一个类,如果该对象的serialVersionUID与对应持久化时的类不同,那么反序列化的过程中将会导致InvalidClassException异常。
    private static final long serialVersionUID = 1L;
    @JsonFormat(shape = JsonFormat.Shape.STRING)
    private Long id;
 
    private String username;
 
    private String name;
 
    private String password;
 
    private String phone;
 
    private String sex;
 
    private String idNumber;
 
    private Integer status;
    @TableField(fill=FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill =FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
//阿里巴巴的开发规范中推荐每个表都带有一个createTime 和一个 updateTime, 但是每次自己手动添加太麻烦了,可以配置MP让其自动添加,使用@TableField的fill注解。
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(shape = JsonFormat.Shape.STRING)
    private Long createUser;
 
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(shape = JsonFormat.Shape.STRING)
    private Long updateUser;
 
}
  1. dao,service,controller
@Mapper
public interface EmployeeDao extends BaseMapper<Employee> {
}
public interface EmployeeService extends IService<Employee> {
}
 
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeDao, Employee> implements EmployeeService {
}
  1. 导入实体类R,此类是一个通用结果类,服务端响应的所有结果最终都会包装成此种类型返回给前端页面
//用泛型格式,如果controller返回的是页面数据,则return R.success(page);如果返回的是成功消息,则return R.success("success");如果返回错误消息,return R.error("错误原因");
@Data
public class R<T> {
 
    private Integer code; //编码:1成功,0和其它数字为失败
 
    private String msg; //错误信息
 
    private T data; //数据
 
    private Map map = new HashMap(); //动态数据
    //静态类,controller返回时直接return R.success(T);
    public static <T> R<T> success(T object) {
        R<T> r = new R<T>();
        r.data = object;
        r.code = 1;
        return r;
    }
 
    public static <T> R<T> error(String msg) {
        R r = new R();
        r.msg = msg;
        r.code = 0;
        return r;
    }
    //添加动态数据
    public R<T> add(String key, Object value) {
        this.map.put(key, value);
        return this;
    }
}
  1. 在Controller中创建登录方法
    处理逻辑:
    1、将页面提交的密码password进行md5加密处理
    2、根据页面提交的用户名username查询数据库
    3、如果没有查询到则返回登录失败结果
    4、密码比对,如果不一致则返回登录失败结果
    5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
    6、登录成功,将员工id存入Session并返回登录成功结果
    瑞吉外卖项目----(1)基本功能实现_第16张图片
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
    @Autowired
    private EmployeeService employeeService;

    /**
     * 员工登录
     * @param request
     * @param employee
     * @return
     */
    @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::getPassword,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",employee.getId());

        return R.success(emp);
    }
}

通过debug进行功能测试

3.2 退出功能

3.2.1 需求分析

员工登录成功后,页面跳转到后台系统首页面(backend/index.html),此时会显示当前登录用户的姓名,如果员工需要退出系统,直接点击右侧的退出按钮即可退出系统,退出系统后页面应跳转回登录页面

3.2.2 代码实现

用户点击页面中退出按钮,发送请求,请求地址为/employee/logout,请求方式为POST。我们只需要在Controller中创建对应的处理方法即可,具体的处理逻辑:1、清理Session中的用户id;2、返回结果

/**
 * 员工退出
 * @param request
 * @return
 */
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
    //清理Session中保存的当前登录员工的id
    request.getSession().removeAttribute("employee");
    return R.success("退出成功");
}

3.3 拦截登录页面

问题分析:登录功能完成后,用户如果不登录,直接访问系统首页面,照样可以正常访问,这种设计并不合理。
需求:登录成功后才可以访问系统中的页面,如果没有登录则跳转到登录页面
解决方案:使用过滤器或者拦截器,在过滤器或者拦截器中判断用户是否已经完成登录,如果没有登录则跳转到登录页面。
代码实现:

  1. 创建自定义过滤器LoginCheckFilter
/**
 * 检查用户是否已经登录
 */
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
public class LoginCheckFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        filterChain.doFilter(request,response);
    }
}

  1. 在启动类上加入注解@ServletComponentScan

测试一下拦截功能是否生效
瑞吉外卖项目----(1)基本功能实现_第17张图片

  1. 完善过滤器的处理逻辑

获取本次请求的URI
判断本次请求是否需要处理
如果不需要处理,则直接放行
判断登录状态,如果已登录,则直接放行
如果未登录则返回未登录结果
瑞吉外卖项目----(1)基本功能实现_第18张图片

/**
 * 检查用户是否已经登录
 */
@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;

        //1、获取本次请求的URI
        String requestURI = request.getRequestURI();
        
        //定义不需要处理的请求路径
        String[] urls = new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/*8"
        };
        //2.判断本次请求是否需要处理
        boolean check = check(urls, requestURI);

        //3、如果不需要处理,则直接放行
        if (check){
            filterChain.doFilter(request,response);
            return;
        }
        
        //4、判断登录状态,如果已登录,则直接放行
        if (null != request.getSession().getAttribute("employee")){
            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) {
            boolean match = PATH_MATCHER.match(url, requestURI);
            if(match){
                return true;
            }
        }
        return false;
    }
}

debug测试一下

4 员工管理

4.1 新增员工

4.1.1 需求分析

后台系统中可以管理员工信息,通过新增员工来添加后台系统用户。点击[添加员工]按钮跳转到新增页面。
瑞吉外卖项目----(1)基本功能实现_第19张图片

4.1.2 数据模型

新增员工,其实就是将我们新增页面录入的员工数据插入到employee表。

注意:

  1. employee表中对username字段加入了唯一约束,因为username是 员工的登录账号,必须是唯一的
  2. employee表中的status字段已经设置了默认值1,表示状态正常

4.1.3 代码实现

1、页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
2、服务端Controller接收页面提交的数据并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据
瑞吉外卖项目----(1)基本功能实现_第20张图片

    @PostMapping
    public R<String> save(HttpServletRequest request,@RequestBody Employee employee){
        log.info("新增员工,员工信息:{}",employee.toString());
 
        //前端没有设置密码框,这里设置初始密码123456,需要进行md5加密处理。正常可以把"12345"改成employee.getPassword()
        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("新增员工成功");
    }

id雪花自增算法,改不改都行,Mybatis-plus默认自增就是ASSIGN_ID 在这里插入图片描述
瑞吉外卖项目----(1)基本功能实现_第21张图片

4.1.4 异常拦截处理

问题:在新增员工时输入的账号已经存在,由于employee表中对该字段加入了唯一约束,此时程序会抛出异常:

java. sql. SQLIntegrityConstraintViolat ionException: Duplicate entry’ zhangsan’ for key' idx _username'

此时需要我们的程序进行异常捕获,通常有两种处理方式:
方式一:在Controller方法中加入try、catch进行异 常捕获
瑞吉外卖项目----(1)基本功能实现_第22张图片

代码量很大的情况下,很多的try catch就会很乱,所以不推荐使用这种方式

方式二:使用异常处理器进行全局异常捕获

//@RestControllerAdvice包括了下面两行
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
 
    /**
     * 异常处理方法
     * @return
     */
    //捕获完整性约束违反异常(其实就是数据库唯一约束异常)SQLIntegrityConstraintViolationException
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
        log.error(ex.getMessage());
 
        if(ex.getMessage().contains("Duplicate entry")){
            return R.error("唯一约束异常:"+exception.getMessage().split(" ")[2]+"已存在");
        }
 
        return R.error("未知唯一约束错误");
    }
}

瑞吉外卖项目----(1)基本功能实现_第23张图片当报错信息出现Duplicate entry时,就意味着新增员工异常了 ,不需要再用try catch这种形式了,不用管他,因为一旦出现错误就会被我们的AOP捕获。

4.2 员工信息分页查询

4.2.1 需求分析

系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,因此一般的系统中都会以分页的方式来展示列表数据
瑞吉外卖项目----(1)基本功能实现_第24张图片

4.2.2 代码实现

整个程序的执行过程:
1、页面发送ajax请求,将分页查询参数(page、pageSize、 name)提交到服务端
2、服务端Controller接收 页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将 查询到的分页数据响应给页面
5、页面接收到分页数据并通过ElementUl的Table组件展示到页面上
分页请求:
瑞吉外卖项目----(1)基本功能实现_第25张图片
查询员工及显示接口:
瑞吉外卖项目----(1)基本功能实现_第26张图片
Mybatis-plus实现分页
mp分页拦截器

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

分页查询

    @GetMapping("/page")
    public R<Page> page(int page,int pageSize,String name){
        log.info("page = {},pageSize = {},name = {}" ,page,pageSize,name);
 
        //构造分页构造器
        Page pageInfo = new Page(page,pageSize);
 
        //构造条件构造器
        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();
        //添加过滤条件
        // StringUtils.isNotEmpty(name)判断为空的标准是name!=null&name.length>0
        queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
        //添加排序条件
        queryWrapper.orderByDesc(Employee::getUpdateTime);
 
        //执行查询
        employeeService.page(pageInfo,queryWrapper);
 
        return R.success(pageInfo);
    }

4.3 启用/禁用员工账号—js精度丢失问题

4.3.1 需求分析

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

4.3.2 代码实现

  • 页面是怎么做到只有管理员admin能看到启用、禁用按钮的

  • 瑞吉外卖项目----(1)基本功能实现_第27张图片

  • 程序执行过程:
    1.页面发生ajax请求,将参数(id、status)提交到服务端
    2.服务端Controller接收页面提交的数据并调用Service更新数据
    3.Service调用Maper操作数据库
    瑞吉外卖项目----(1)基本功能实现_第28张图片

  • 页面中的ajax请求是如何发送

  • 瑞吉外卖项目----(1)基本功能实现_第29张图片

  • 启用、禁用员工账号本质就是一个更新操作。(也就是对status状态字段进行操作),在Controller中创建update方法,此方法是一个通用的修改员工信息的方法

/**
 * 根据id修改员工信息
 * @param employee
 * @return
 */
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
    log.info(employee.toString());
    //获取当前登录用户的id
    Long empId = (Long) request.getSession().getAttribute("employee");
    employee.setUpdateTime(LocalDateTime.now());
    employee.setUpdateUser(empId);
    employeeService.updateById(employee);
    return R.success("员工信息修改成功");
}

问题:测试过程中没有报错,但是功能并没实现,查看数据库中的数据也没有变化,通过观察控制台输出的SQL发现页面传递过来的员工id的值和数据库中的id值不一致
瑞吉外卖项目----(1)基本功能实现_第30张图片

原因:分页查询时服务端响应给页面的数据中id的值为19位数字,类型为Long;页面中js处理long型数字只能精确到16位,所以最终通过ajax请求提交给服务端的id存在精度丢失
解决方案:
1.提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换
2.在WebMvcConfig配置类中扩展SpringMVC的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换

/**
 * 扩展mvc框架的消息转换器
 * @param converters
 */
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    log.info("扩展消息转换器");
    //创建消息转换器对象
    MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
    //设置对象转换器,底层使用Jackson将Java对象转为json
    messageConverter.setObjectMapper(new JacksonObjectMapper());
    //将上面的消息转换器对象追加到mvc框架的转换器集合中
    converters.add(0,messageConverter);
}

对象映射器:基于jackson将Tava对象转为json,或者将json转为Java对象
将JSON解析为Java对象的过程称为[从JSON反序列化Java对象]
从Java对象生成JSON的过程称为[序列化Java对象到JSON]

4.4 编辑员工信息

4.4.1 需求分析

在员工管理列表页面点击编辑按钮,跳转到编辑页面,在编辑页面回显员工信息并进行修改,最后点击保存按钮完成编辑操作

4.4.2 代码开发

操作过程和对应的程序执行流程

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

注意:add.html页面为公共页面,新增员工和编辑员工都是在此页面操作

窗口回显员工信息:

/**
 * 根据id查询员工信息
 * @param id
 * @return
 */
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id){
    log.info("根据id查询员工信息...");
    Employee employee = employeeService.getById(id);
    if (employee != null){
        return R.success(employee);
    }
    return R.error("没有查询到对应员工信息");
}

回显后,通过调用上面写的update方法完成修改

5 分类操作

5.1 公共字段自动填充

5.1.1 问题分析

已经完成了后台系统的员工管理功能开发,在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间和修改人等字段。这些字段属于公共字段,也就是很多表中都有这些字段,如下
瑞吉外卖项目----(1)基本功能实现_第31张图片

问题:能不能对于这些公共字段在某个地方统一处理,来简化开发呢?
答:使用Mybatis Plus提供的公共字段自动填充功能

5.1.2 代码实现

Mybatis Plus公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。

  1. 在实体类的属性上加入@TableField注解,指定自动填充的策略
    瑞吉外卖项目----(1)基本功能实现_第32张图片

  2. 按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口

@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    /**
     * 插入操作,自动填充
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共字段自动填充[insert]...");
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("createUser", new Long(1));
        metaObject.setValue("updateUser", new Long(1));
    }
    /**
     * 修改操作,自动填充
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("公共字段自动填充[update]...");
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("updateUser", new Long(1));
    }
}

注意:当前我们设置createUser和updateUser为固定值,后面我们需要进行改造,改为动态获得当前登录用户的id

  1. 测试:将EmployeeController中的新增和修改方法中手动设置公共字段的代码注释掉,测试这些公共字段是否完成了自动填充

5.1.3 功能完善

前面我们已经完成了公共字段自动填充功能的代码开发,但是还有一个问题没有解决,就是我们在自动填充createUser和updateUser时设置的用户id是固定值,现在我们需要改成动态获取当前登录用户的id
用户登录成功后我们将用户id存入了HttpSession中,现在我从HttpSession中获取不就行了?

注意:我们在MyMetaObjectHandler类中是不能获得HttpSession对象的,所以我们需要通过其他方式来获取登录用户id。

可以使用ThreadLocal来解决此问题,它是JDK中提供的一个类。

在学习ThreadLocal之前,需要先确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:
1、LoginCheckilter的doFilter方法
2、EmployeeController的update方法
3、MyMetaObjectHandler的updateFil方法

可以在上面的三个方法中分别加入下面代码(获取当前线程id)

long id = Thread.currentThread().getId();
log.info("线程id为:{}",id);

执行编辑员工功能进行验证,通过观察控制台输出可以发现,一次请求对应的线程id是相同的:
瑞吉外卖项目----(1)基本功能实现_第33张图片

什么是 ThreadLocal?

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

ThreadLocal常用方法:

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

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

实现步骤:
1、编写BaseContext工具类,基于ThreadLocal封装的工具类
2、在LoginCheckilter的doFilter方 法中调用BaseContext来设置当前登录用户的id
3、在MyMetaObjectHandler的方 法中调用BaseContext获取登录用户的id

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

    /**
     * 设置值
     * @param id
     */
    public static void setThreadLocal(Long id){
        threadLocal.set(id);
    }

    /**
     * 获取值
     * @return
     */
    public static Long getCurrentId(){
        return threadLocal.get();
    }
}

自定义元数据对象处理器,获取ThreadLocal的id

登录检查过滤器把id加到ThreadLocal

        //4、判断登录状态,如果已登录,则直接放行
        if(request.getSession().getAttribute("employee") != null){
            log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
            //这里要强转,虽然request.getSession().getAttribute("employee")类型确实是Long
            Long empId = (Long) request.getSession().getAttribute("employee");
            BaseContext.setCurrentId(empId);
 
            filterChain.doFilter(request,response);
            return;
        }

5.2 新增分类

5.2.1 需求分析

后台系统中可以管理分类信息,分类包括两种类型,分別是菜品分类和套餐分类。当我们在后台系统中添加菜品时需要选择一个菜品分类,当我们在后台系统中添加一个套餐时需要选择一个套餐分类 ,在移动端也会按照菜品分类和套餐分类来展示对应的菜品和套餐。
可以在后台系统的分类管理页面分别添加菜品分类和套餐分类

5.2.2 数据模型

新增分类就是把新增窗口录入的分类数据插入到category表,表结构:
瑞吉外卖项目----(1)基本功能实现_第34张图片

注意:category表对name字段加入了唯一约束,保证分类的名称是唯一的

8.2.3 代码开发

环境准备:先将需要用到的类和接口基本结构创建好
● 实体类Category ( 直接从课程资料中导入即可)
● Mapper接口CategoryMapper
● 业务层接口CategoryService
● 业务 层实现类CategoryServicelmpl
● 控制层CategoryController

整个程序的执行过程:
1、页面(backend/ page/category/list.html)发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端
2、服务端Controller接收页面提交的数据并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据

可以看到新增菜品分类和新增套餐分类请求的服务端地址和提交的json数据结构相同,所以服务端只需要提供一个方法统一处理即可
瑞吉外卖项目----(1)基本功能实现_第35张图片

代码实现:

/**
 * 新增分类
 * @param category
 * @return
 */
@PostMapping
public R<String> save(@RequestBody Category category){
    log.info("category:{}",category);
    categoryService.save(category);
    return R.success("新增分类成功");
}

5.3 分类信息分页查询

5.3.1 需求分析

系统中的分类很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。

5.3.2 代码开发

整 个程序的执行过程:
1、页面发送ajax请求,将分页查询参数(page、 pageSize)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面
5、页面接收到分页数据并通过ElementUl的Table组件展示到页面上
瑞吉外卖项目----(1)基本功能实现_第36张图片

/**
 * 分页查询
 * @param page
 * @param pageSize
 * @return
 */
@GetMapping("/page")
public R<Page> page(int page,int pageSize){
    //构造分页构造器
    Page pageInfo = new Page(page,pageSize);
    //构造条件构造器
    LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper();
    //添加排序条件
    queryWrapper.orderByDesc(Category::getSort);
    //执行查询
    categoryService.page(pageInfo,queryWrapper);
    return R.success(pageInfo);
}

5.4 删除分类

5.4.1 需求分析

在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。

5.4.2 基础删除代码实现

整个程序的执行过程:
1、页面发送ajax请求,将参数(id)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service删除数据
3、Service调用Mapper操作数据库
瑞吉外卖项目----(1)基本功能实现_第37张图片

/**
 * 根据id删除分类
 * @param ids
 * @return
 */
@DeleteMapping
public R<String> delete(Long ids){
    log.info("删除分类,id为:{}",ids);
    categoryService.removeById(ids);
    return R.success("分类信息删除成功");
}

5.4.3 功能完善----检查是否有关联再删除

前面我们已经实现了根据id删除分类的功能,但是并没有检查删除的分类是否关联了菜品或者套餐,所以我们需要进行功能完善。要完善分类删除功能,需要先准备基础的类和接口:
1、实体类Dish和Setmeal (从课程资料中复制即可)
2、Mapper接口DishMapper和SetmealMapper
3、Service接口DishService和SetmealService
4、Service实现类DishServicelmpl和SetmealServicelmpl
代码实现:
自定义业务异常类CustomException:

/**
 * 自定义业务异常类
 */
public class CustomException extends RuntimeException{
    //带参构造方法,重新设置异常信息
    public CustomException(String message){
        super(message);
    }
}

异常处理GlobalExceptionHandler:

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

CategoryServiceImpl:

@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
    @Autowired
    private DishService dishService;
    @Autowired
    private SetmealService setmealService;

    @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(setmealLambdaQueryWrapper);
        if (count2 > 0){
            throw new CustomException("当前分类下关联了套餐,不能删除");
        }
				
		//正常删除分类
        super.removeById(id);
    }
}

controller:

/**
 * 根据id删除分类
 * @param ids
 * @return
 */
@DeleteMapping
public R<String> delete(Long ids){
    log.info("删除分类,id为:{}",ids);
    //categoryService.removeById(ids);
    categoryService.remove(ids);
    return R.success("分类信息删除成功");
}

5.5 修改分类

5.5.1 需求分析

在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作

8.5.2 代码实现

数据回显通过前端代码已经实现:
瑞吉外卖项目----(1)基本功能实现_第38张图片
瑞吉外卖项目----(1)基本功能实现_第39张图片
修改分类代码实现:

/**
 * 根据id修改分类
 * @param category
 * @return
 */
@PutMapping
public R<String> update(@RequestBody Category category){
    log.info("修改分类信息:{}",category);
    categoryService.updateById(category);
    return R.success("修改分类信息成功");
}

6 菜品管理业务开发

6.1 文件的上传下载

6.1.1 文件上传下载介绍

文件上传,也称为upload,是指将本地图片、视频、音频等文件.上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。
文件上传时,对页面的form表单有如下要求:
在这里插入图片描述

举例:
在这里插入图片描述
目前一些前端组件库也提供了相应的上传组件,但是底层原理还是基于form表单的文件上传。
例如ElementUI中提供的upload上传组件:
瑞吉外卖项目----(1)基本功能实现_第40张图片
服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件:
在这里插入图片描述
Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数(Form data)即可接收上传的文件,例如:
瑞吉外卖项目----(1)基本功能实现_第41张图片

文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程。
通过浏览器进行文件下载,通常有两种表现形式:
1、以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录) 直接在浏览器中打开;
2、通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。

6.1.2 文件上传代码实现

文件上传,页面端可以使用ElementUI提供的上传组件。
可以直接使用资料中提供的上传页面,位置:资料/文件上传下载页面/upload.html
瑞吉外卖项目----(1)基本功能实现_第42张图片
前端上传页面请求:
瑞吉外卖项目----(1)基本功能实现_第43张图片

注意:传参MutlipartFile类型参数名必须是file,也就是和form data的名字一致。

代码实现:
配置文件:

reggie:
  #写成D:\img\或者D:/img/也能成功,写成双反斜杠点保险点,防止转义
  path: D:\\img\\

读取路径并接收上传的文件:

@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {
    @Value("${reggie.path}")
    private String basePath;
    /**
     * 文件地址
     * @param file
     * @return
     */
    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){
        //file是一个临时文件,默认存在C:\\User\\...目录下,需要转存到指定位置,否则本次请求完成后临时文件会删除
        log.info(file.toString());
        //原始文件名
        String originalFilename = file.getOriginalFilename();   //abc.jpg
        //获取文件后缀名,这里不能split,因为split里不能根据“.”分割
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
        //使用UUID重新生成文件名,防止文件名称重复造成文件覆盖。UUID 是通用唯一识别码,具体解释看代码下面引用
        String fileName = UUID.randomUUID().toString() + suffix;    dfsdfdfd.jpg
        //创建一个目录对象
        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);
    }
}

UUID:
UUID 是 通用唯一识别码,UUID 的目的,是让分布式系统中的所有元素,都能有唯一的辨识资讯,而不需要透过中央控制端来做辨识资讯的指定。
UUID.randomUUID().toString()是javaJDK提供的一个自动生成主键的方法。UUID(Universally Unique Identifier)全局唯一标识符,是指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的,是由一个十六位的数字组成,表现出来的形式。由以下几部分的组合:当前日期和时间(UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同),时钟序列,全局唯一的IEEE机器识别号(如果有网卡,从网卡获得,没有网卡以其他方式获得),UUID的唯一缺陷在于生成的结果串会比较长。
回顾:前面Mybatis-plus学习五种id生成策略时,有个策略是ASSIGN_UUID:
ASSIGN_UUID:可以在分布式的情况下使用,而且能够保证唯一,但是生成的主键是32位的字符串,长度过长占用空间而且还不能排序,查询性能也慢

6.1.3 文件下载代码实现

将文件从服务端下载到本地计算机,可以用标签展示下载的图片:
在这里插入图片描述
两种文件下载方式:
在这里插入图片描述
前端请求:
瑞吉外卖项目----(1)基本功能实现_第44张图片
get方式传参,controller参数名为name就能直接传了,springmvc是可以自动转换格式的。

代码实现:

/**
 * 文件下载
 * @param name
 * @param response
 */
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
    try {
        //输入流,通过输入流读取文件内容
        FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));
        //输出流,通过输出流将文件写回浏览器
        ServletOutputStream outputStream = response.getOutputStream();
        response.setContentType("image/jpg");
        int len = 0;
        byte[] bytes = new byte[1024];
        while ((len = fileInputStream.read(bytes)) != -1){
            outputStream.write(bytes,0,len);
            outputStream.flush();
        }
        //关闭资源
        outputStream.close();
        fileInputStream.close();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

6.2 新增菜品

6.2.1 需求分析

后台系统中可以管理菜品信息,通过新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片,在移动端会按照菜品分类来展示对应的菜品信息

6.2.2 数据模型

新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish. flavor表插入数据。所以在新增菜品时,涉及到两个表:菜品表dish;菜品口味表dish_flavor
瑞吉外卖项目----(1)基本功能实现_第45张图片

6.2.3 代码开发

在开发业务功能前,先将需要用到的类和接口基本结构创建好:

  • 实体类DishFlavor (直接从课程资料中导入即可,Dish实体前面课程中已经导入过了)
  • Mapper接口 DishFlavorMapper
  • 业务层接口DishFlavorService
  • 业务层实现类DishFlavorServicelmpl
  • 控制层DishController

新增菜品时前端页面和服务端的交互过程:

  • 页面(backend/page/food/ add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
  • 页面发送请求进行图片上传,请求服务端将图片保存到服务器
  • 页面发送请求进行图片下载,将上传的图片进行回显
  • 点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端

开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可

根据条件查询分类数据:

瑞吉外卖项目----(1)基本功能实现_第46张图片

点击添加菜品按钮的请求,展示分类框:
瑞吉外卖项目----(1)基本功能实现_第47张图片

CategoryController:

/**
 * 根据条件查询分类数据
 * @param category
 * @return
 */
@GetMapping("/list")
public R<List<Category>> list(Category category){
    //条件构造器
    LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
    //添加条件
    queryWrapper.eq(category.getType() != null,Category::getType,category.getType());
    //添加排序条件
    queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
    List<Category> list = categoryService.list(queryWrapper);
    return R.success(list);
}

创建DTO类封装表单数据
因为表单既包括了菜品,也包括了菜品味道,所以要新创建一个DTO类封装所有表单信息,它是菜品实体类的子类。
DishDto:

@Data
public class DishDto extends Dish {
 
    private List<DishFlavor> flavors = new ArrayList<>();
    //下面两个成员变量暂时用不到,先放着,后面有其他表单需要封装
 
    //菜品所属分类名称
    private String categoryName;
 
    private Integer copies;
}

DTO, 全称为Data Transfer Object,即数据传输对象,一 般用于展示层与服务层之间的数据传输。

新增菜品
瑞吉外卖项目----(1)基本功能实现_第48张图片
DishService和DishServiceImpl:

public interface DishService extends IService<Dish> {
    //新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish,dish_flavor
    public void saveWithFlavor(DishDto dishDto);
}

@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
    @Autowired
    private DishFlavorService dishFlavorService;
    
    /**
     * 新增菜品同时保存对应的口味数据
     * @param dishDto
     */
    @Override
    @Transactional    //记得在引导类@EnableTransactionManagement
    public void saveWithFlavor(DishDto dishDto) {
        //保存菜品的基本信息到菜品表dish
        this.save(dishDto);

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

        //菜品口味
        List<DishFlavor> flavors = dishDto.getFlavors();
//        stream流的filter是满足条件的留下,是对原数组的过滤;map则是对原list的加工,map里是Lambda表达式,形容对list加工。
//        通过stream流的map将list加工,把这个菜品的id加到每个list元素的dishId属性;
        flavors = flavors.stream().map((item) -> {    //给list每个元素map加工,返回值为加工后的结果
            item.setDishId(dishId);
            return item;
        }).collect(Collectors.toList());    //将加工后的结果遍历收集到原list

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

DishController:

/**
 * 新增菜品
 * @param dishDto
 * @return
 */
@PostMapping
public R<String> save(@RequestBody DishDto dishDto){
    log.info(dishDto.toString());
    dishService.saveWithFlavor(dishDto);
    return R.success("新增菜品成功");
}

6.3 菜品信息分页查询

6.3.1 需求分析

系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。

6.3.2 代码实现

梳理一下菜品分页查询时前端页面和服务端的交互过程:

1、页面(backend/ page/food/list.html)发送ajax请求,将分页查询参数(page、pageSize、 name)提交到服务端,获取分页数据

2、页面发送请求,请求服务端进行图片下载,用于页面图片展示

开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
瑞吉外卖项目----(1)基本功能实现_第49张图片

首先把图片资料拷贝到D://img中,这是前面上传下载代码时候在yml配置的图片路径。

DishController

/**
 * 分页查询
 * @param page
 * @param pageSize
 * @param name
 * @return
 */
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name){
    //构造分页构造器对象
    Page<Dish> pageInfo = new Page<>(page,pageSize);
    Page<DishDto> dishDtoPage = new Page<>();
    //条件构造器
    LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
    //添加过滤条件
    queryWrapper.like(name != null,Dish::getName,name);
    //添加排序条件
    queryWrapper.orderByDesc(Dish::getUpdateTime);
    //执行分页查询,dishService查的是dish表,所以必须先Dish查询,再拷贝到DishDto
    dishService.page(pageInfo,queryWrapper);
    //注意这一步,将Dish的page拷贝到DishDto的page,第三个参数是忽略records属性
    BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
    List<Dish> records = pageInfo.getRecords();
    //steam流的map(Lambda表达式)对list进行加工,stream流返回值是可以换泛型的。等号左边是List,右边是List
    List<DishDto> list = records.stream().map((item) -> {
        DishDto dishDto = new DishDto();
        //对象拷贝的工具类BeanUtils,将list里每一个菜品Dish对象信息拷贝到DishDto对象
        BeanUtils.copyProperties(item,dishDto);
        //将菜品所属分类的id查询为name,存到DishDto对象的categoryName,再返回替换item,完成item的加工
        Long categoryId = item.getCategoryId();//分类id
        //根据id查询分类对象
        Category category = categoryService.getById(categoryId);
        if(category != null){
            String categoryName = category.getName();
            dishDto.setCategoryName(categoryName);
        }
        return dishDto;
    }).collect(Collectors.toList());
    dishDtoPage.setRecords(list);
    return R.success(dishDtoPage);
}

6.4 修改菜品

6.4.1 需求分析

在菜品管理列表页面点击修改按钮,跳转到修改菜品页面,在修改页面回显菜品相关信息并进行修改,最后点击确定按钮完成修改操作

6.4.2 代码实现

修改菜品时前端页面(add.html) 和服务端的交互过程:
1、页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示
2、页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
3、页面发送请求,请求服务端进行图片下载,用于页图片回显
4、点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端

开发修改菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可

由修改页面请求可以得知这里返回值必须是DishDto对象(包括菜品、味道、分类信息)。 所以需要在DishServiceImpl写getByIdWithFlavor方法。这里分类名返回null即可,前端会发送请求从分类id查分类name。
瑞吉外卖项目----(1)基本功能实现_第50张图片

数据回显:

/**
 * 根据id查询菜品信息和对应的口味信息
 * @param id
 * @return
 */
@Override
public DishDto getByIdWithFlavor(Long id) {
    //查询菜品基本信息,从dish表查询
    Dish dish = this.getById(id);
    DishDto dishDto = new DishDto();
    BeanUtils.copyProperties(dish,dishDto);
    //查询当前菜品对应的口味信息,从dish_flavor表查询
    LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(DishFlavor::getDishId,dish.getId());
    List<DishFlavor> dishFlavors = dishFlavorService.list(queryWrapper);
    dishDto.setFlavors(dishFlavors);
    return dishDto;
}
/**
 * 根据id查询菜品信息和对应的口味信息
 * @param id
 * @return
 */
@GetMapping("/{id}")
public R<DishDto> get(@PathVariable Long id){
    DishDto dishDto = dishService.getByIdWithFlavor(id);
    return R.success(dishDto);
}

更新数据:

与新增时一样的问题DishFlavor中只是封装的name和value,而dishId并没有封装上,需要处理一下

/**
 * 更新菜品信息同时更新口味信息
 * @param dishDto
 */
@Override
@Transactional
public void updateWithFlavor(DishDto dishDto) {
    //更新dish表基本数据
    this.updateById(dishDto);
    //清理当前菜品对应口味数据
    // dish_flavor表的delete操作:delete from dish_flavor where dish_id = ???
    LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
    dishFlavorService.remove(queryWrapper);
    //添加当前提交过来的口味数据(dish_flavor表的insert操作)
    List<DishFlavor> flavors = dishDto.getFlavors();
    flavors = flavors.stream().map((item) -> {    //给list每个元素map加工,返回值为加工后的结果
        item.setDishId(dishDto.getId());
        return item;
    }).collect(Collectors.toList());    //将加工后的结果遍历收集到原list
    //保存菜品口味数据到菜品口味表dish_flavor
    dishFlavorService.saveBatch(flavors);
}
/**
 * 修改菜品
 * @param dishDto
 * @return
 */
@PutMapping
public R<String> update(@RequestBody DishDto dishDto){
    log.info(dishDto.toString());
    dishService.updateWithFlavor(dishDto);
    return R.success("新增菜品成功");
}

7 套餐管理业务开发

7.1 新增套餐

7.1.1 需求分析

套餐就是菜品的集合。
后台系统中可以管理套餐信息,通过新增套餐功能来添加一个新的套餐,在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐。

7.1.2 数据模型

新增套餐其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal _dish表插入套餐和菜品关联数据。所以在新增套餐时,涉及到两个表:套餐setmeal表;套餐菜品关系setmeal _dish表
setmeal表:
瑞吉外卖项目----(1)基本功能实现_第51张图片
setmeal _dish表:
瑞吉外卖项目----(1)基本功能实现_第52张图片

7.1.3 代码开发

先将需要用到的类和接口基本结构创建好:
●实体类SetmealDish ( 直接从课程资料中导入即可,Setmeal实体前 面课程中已经导入过了)
●DTO SetmealDto (直接从课程资料中导入即可)
●Mapper接口 SetmealDishMapper
●业务层接口SetmealDishService
●业务层实现类SetmealDishServicelmpl
●控制层SetmealController

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

开发新增套餐功能,其实就是在服务端编写代码去处理前端页面发送的这6次请求即可。

瑞吉外卖项目----(1)基本功能实现_第53张图片

setmealDishes不是setmeal表的字段,需要用SetmealDto封装参数

setmealServiceImpl:

/**
 * 新增套餐同时需要保存套餐和菜品的关联关系
 * @param setmealDto
 */
@Override
@Transactional
public void saveWithDish(SetmealDto setmealDto) {
    //保持套餐的基本信息,操作setmeal,执行insert操作
    this.save(setmealDto);
    List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
    setmealDishes.stream().map((item)-> {
        item.setSetmealId(setmealDto.getId());
        return item;
    }).collect(Collectors.toList());
    //保存套餐和菜品的关联关系,操作setmeal_dish,执行insert操作
    setmealDishService.saveBatch(setmealDishes);
}

setmealController:

/**
 * 新增套餐
 * @param setmealDto
 * @return
 */
@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto){
    log.info("套餐信息:{}",setmealDto);
    setmealService.saveWithDish(setmealDto);
    return R.success("新增套餐成功");
}

7.2 套餐信息分页查询

7.2.1 需求分析

系统中的套餐数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看T所以一般的系统中都会以分页的方式来展示列表数据。

7.2.2 代码实现

梳理一下套餐分页查询时前端页面和服务端的交互过程
1、页面(backend/ page/ combo/list.html)发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示

开发套餐信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
瑞吉外卖项目----(1)基本功能实现_第54张图片
setmealController:

/**
 * 套餐分页查询
 * @param page
 * @param pageSize
 * @param name
 * @return
 */
@GetMapping("/page")
public R<Page> page(int page, int page
    //分页构造器
    Page<Setmeal> pageInfo = new Page<
    Page<SetmealDto> dtoPage = new Pag
    LambdaQueryWrapper<Setmeal> queryW
    //添加条件查询,根据name进行模糊查询like
    queryWrapper.like(name != null, Se
    //添加排序条件,根据更新时间进行降序排序
    queryWrapper.orderByDesc(Setmeal::
    setmealService.page(pageInfo,query
    //对象拷贝
    BeanUtils.copyProperties(pageInfo,
    //忽略掉records后需要自己设置records的值
    List<Setmeal> records = pageInfo.g
    //处理records,将records计算后赋给list对象
    List<SetmealDto> list =records.str
        SetmealDto setmealDto = new Se
        //对象拷贝
        BeanUtils.copyProperties(item,
        //套餐分类id
        Long categoryId = item.getCate
        //根据分类id查询分类对象
        Category category = categorySe
        if (category != null){
            //套餐分类名称
            String categoryName = cate
            setmealDto.setCategoryName
        }
        return setmealDto;
    }).collect(Collectors.toList());
    dtoPage.setRecords(list);
    return R.success(dtoPage);
}

7.3 删除套餐

7.3.1 需求分析

在套餐管理列表页面点击删除按钮,可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。
注意:对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。

7.3.2 代码实现

在开发代码之前,需要梳理一下删 除套餐时前端页面和服务端的交互过程:
1、删除单个套餐时,页面发送ajax请求,根据套餐id删除对应套餐
在这里插入图片描述

2、删除多个套餐时,页面发送ajax请求,根据提交的多个套餐id删除对应套餐
在这里插入图片描述

开发删除套餐功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
观察删除单个套餐和批量删除套餐的请求信息可以发现,两种请求的地址和请求方式都是相同的,不同的则是传递的id个数,所以在服务端可以提供一个方法来统一处理。
setmealService:

/**
 * 删除套餐同时删除套餐的关联数据
 * @param ids
 */
@Override
@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("套餐正在售卖中,不能删除!");
    }
    //如果可以删除,先删除表中的数据
    this.removeByIds(ids);
    //delete from setmeal_dish where id in (1,2,3...)
    LambdaQueryWrapper<SetmealDish> setmealDishLambdaQueryWrapper = new LambdaQueryWrapper<>();
    setmealDishLambdaQueryWrapper.in(SetmealDish::getId,ids);
    //删除关系表中的数据 setmeal_dish
    setmealDishService.remove(setmealDishLambdaQueryWrapper);
}

setmealController:

/**
 * 删除套餐
 * @param ids
 * @return
 */
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids) {
    log.info("ids:{}",ids);
    setmealService.removeWithDish(ids);
    return R.success("套餐数据删除成功");
}

8 手机验证码登录

8.1 短信发送

8.1.1 短信服务介绍

8.1.2 腾讯云短信服务

瑞吉外卖项目----(1)基本功能实现_第55张图片

8.1.3 代码开发

8.2 手机验证码

8.2.1 需求分析

为了方便用户登录,移动端通常都会提供通过手机验证码登录的功能。
手机验证码登录的优点:
●方便快捷,无需注册,直接登录
●使用短信验证码作为登录凭证,无需记忆密码
●安全

登录流程:输入手机号–>获取验证码–>输入验证码–>点击登录–>登录成功

注意:通过手机验证码登录,手机号是区分不同用户的标识

8.2.2 数据模型

通过手机验证码登录时,涉及的表为user表,即用户表
瑞吉外卖项目----(1)基本功能实现_第56张图片

8.2.3 代码实现

在开发代码之前,需要梳理一下登 录时前端页面和服务端的交互过程:
1、在登录页面(front/page/login.html)输入手机号,点击[获取验证码]按钮,页面发送ajax请求,在服务端调用短信服务API给指定手机号发送验证码短信
2、在登录页面输入验证码,点击[登录]按钮,发送ajax请求,在服务端处理登录请求
开发手机验证码登录功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。

先将需要用到的类和接口基本结构创建好:
●实体类User (直接从课程资料中导入即可)
●Mapper接口 UserMapper
●业务层接口UserService,业务层实现类UserServicelmpl
●控制层UserController
●工具类SMSUtils、 ValidateCodeUtils (直接从课程资料中导入即可)

修改LoginCheckFilter:
前面我们已经完成了LoginCheckFilter过滤器的开发,此过滤器用于检查用户的登录状态。我们在进行手机验证码登录时,发送的请求需要在此过滤器处理时直接放行
瑞吉外卖项目----(1)基本功能实现_第57张图片
在LoginCheckFilter过滤器中扩展逻辑,判断移动端用户登录状态:

//4.2 判断移动端登录状态,如果已登录,则直接放行
if (null != request.getSession().getAttribute("user")){
    log.info("移动端用户已登录,用户id:{}",request.getSession().getAttribute("user"));
    long id = Thread.currentThread().getId();
    log.info("线程id为:{}",id);
    //这里要强转,虽然request.getSession().getAttribute("employee")类型确实是Long
    Long userId = (Long) request.getSession().getAttribute("user");
    BaseContext.setCurrentId(userId);
    filterChain.doFilter(request,response);
    return;
}

模拟发送验证码:
瑞吉外卖项目----(1)基本功能实现_第58张图片

/**
 * 发送手机短信验证码
 * @param user
 * @return
 */
@PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user, HttpSession httpSessio
    //获取手机号
    String phone = user.getPhone();
    if(StringUtils.isNotEmpty(phone)){
        //生成随机的4位验证码
        String code = ValidateCodeUtils.generateValidateCode(4).toStrin
        log.info("code = {}",code);
        //调用阿里云提供的短信服务API完成发送短信
        //SMSUtils.sendMessage("瑞吉外卖","",phone,code);
        //需要将生成的验证码保存到session
        httpSession.setAttribute(phone,code);
        return R.success("手机验证码发送成功");
    }
    return R.error("手机验证码发送失败");
}

用户登录(首次登录自动注册):

    @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);
            }
            session.setAttribute("user",user.getId());
            return R.success(user);
        }
        return R.error("登录失败");
    }

9 菜品展示、购物车、下单

9.1 用户地址薄

9.1.1 需求分析

地址簿,指的是移动端消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址

9.1.2 数据模型

用户的地址信息会存储在address_ book表,即地址簿表中
瑞吉外卖项目----(1)基本功能实现_第59张图片

9.1.2 代码实现

● 实体类AddressBook (直接从课程资料中导入即可)
● Mapper接 口AddressBookMapper
● 业务层接口AddressBookService
● 业务 层实现类AddressBookServicelmpl
● 控制层AddressBookController

/**
 * 地址簿管理
 */
@Slf4j
@RestController
@RequestMapping("/addressBook")
public class AddressBookController {
 
    @Autowired
    private AddressBookService addressBookService;
 
    /**
     * 新增
     */
    @PostMapping
    public R<AddressBook> save(@RequestBody AddressBook addressBook) {
        addressBook.setUserId(BaseContext.getCurrentId());
        log.info("addressBook:{}", addressBook);
        addressBookService.save(addressBook);
        return R.success(addressBook);
    }
 
    /**
     * 设置默认地址
     */
    @PutMapping("default")
    public R<AddressBook> setDefault(@RequestBody AddressBook addressBook) {
        //先把所有地址is_default改成0
        log.info("addressBook:{}", addressBook);
        LambdaUpdateWrapper<AddressBook> wrapper = new LambdaUpdateWrapper<>();
        wrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
        wrapper.set(AddressBook::getIsDefault, 0);
        //SQL:update address_book set is_default = 0 where user_id = ?
        addressBookService.update(wrapper);
 
        //再把这个地址设为默认
        addressBook.setIsDefault(1);
        //SQL:update address_book set is_default = 1 where id = ?
        addressBookService.updateById(addressBook);
        return R.success(addressBook);
    }
 
    /**
     * 根据id查询地址
     */
    @GetMapping("/{id}")
    public R get(@PathVariable Long id) {
        AddressBook addressBook = addressBookService.getById(id);
        if (addressBook != null) {
            return R.success(addressBook);
        } else {
            return R.error("没有找到该对象");
        }
    }
 
    /**
     * 查询默认地址
     */
    @GetMapping("default")
    public R<AddressBook> getDefault() {
        LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
        queryWrapper.eq(AddressBook::getIsDefault, 1);
 
        //SQL:select * from address_book where user_id = ? and is_default = 1
        AddressBook addressBook = addressBookService.getOne(queryWrapper);
 
        if (null == addressBook) {
            return R.error("没有找到该对象");
        } else {
            return R.success(addressBook);
        }
    }
 
    /**
     * 查询指定用户的全部地址
     */
    @GetMapping("/list")
    public R<List<AddressBook>> list(AddressBook addressBook) {
        addressBook.setUserId(BaseContext.getCurrentId());
        log.info("addressBook:{}", addressBook);
 
        //条件构造器
        LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(null != addressBook.getUserId(), AddressBook::getUserId, addressBook.getUserId());
        queryWrapper.orderByDesc(AddressBook::getUpdateTime);
 
        //SQL:select * from address_book where user_id = ? order by update_time desc
        return R.success(addressBookService.list(queryWrapper));
    }
}

9.2 菜品展示

9.2.1 需求分析

用户登录成功后跳转到系统首页,在首页需要根据分类来展示菜品和套餐。如果菜品设置了口味信息,需要展示选择规格按钮,否则显示+按钮。

9.2.2 代码实现

梳理一下前端页面和服务端的交互过程:
1、页面(front/index.html)发送ajax请求, 获取分类数据(菜品分类和套餐分类)
2、页面发送ajax请求,获取第一个分类下的菜品或者套餐
开发菜品展示功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
瑞吉外卖项目----(1)基本功能实现_第60张图片
注意:首页加载完成后,还发送了一次ajax请求用于加载购物车数据
瑞吉外卖项目----(1)基本功能实现_第61张图片
此处可以将这次请求的地址暂时修改一下,从静态json文件获取数据,等后续开发购物车功能时再修改回来,如下:
瑞吉外卖项目----(1)基本功能实现_第62张图片
问题:为什么登录后直接就展示了部分菜品信息
原因:请求了/category/list和/dish/list
在这里插入图片描述
在这里插入图片描述

又有个问题:为什么需求中的关于口味信息的数据没有
原因:DishController的list方法条件查询(添加套餐时按分类查询菜品id)传参是Dish,要获取到口味信息就需要传dish和dishFlavor,也就是需要换成DishDto

修改DishController的list方法:

    @GetMapping("/list")
    public R<List<DishDto>> list(Dish dish){
        //构造查询条件
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(dish.getCategoryId() != null ,Dish::getCategoryId,dish.getCategoryId());
        //添加条件,查询状态为1(起售状态)的菜品
        queryWrapper.eq(Dish::getStatus,1);
 
        //添加排序条件
        queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
 
        List<Dish> list = dishService.list(queryWrapper);
 
        List<DishDto> dishDtoList = list.stream().map((item) -> {
            DishDto dishDto = new DishDto();
 
            BeanUtils.copyProperties(item,dishDto);
 
            Long categoryId = item.getCategoryId();//分类id
            //根据id查询分类对象
            Category category = categoryService.getById(categoryId);
 
            if(category != null){
                String categoryName = category.getName();
                dishDto.setCategoryName(categoryName);
            }
 
            //当前菜品的id
            Long dishId = item.getId();
            LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
            lambdaQueryWrapper.eq(DishFlavor::getDishId,dishId);
            //SQL:select * from dish_flavor where dish_id = ?
            List<DishFlavor> dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);
            dishDto.setFlavors(dishFlavorList);
            return dishDto;
        }).collect(Collectors.toList());
 
        return R.success(dishDtoList);
    }

问题:点击商务套餐时报错
原因:setmealController中还没写list方法(根据categoryId查询对应的套餐信息)
在这里插入图片描述
SetmealController:

/**
 * 根据条件查询套餐数据
 * @param setmeal
 * @return
 */
@GetMapping("/list")
public R<List<Setmeal>> list(Setmeal setmeal){
    LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(setmeal.getCategoryId() != null,Setmeal::getCategoryId,setmeal.getCategoryId())
            .eq(setmeal.getStatus() != null,Setmeal::getStatus,1);
    queryWrapper.orderByDesc(Setmeal::getUpdateTime);
    List<Setmeal> setmealList = setmealService.list(queryWrapper);
    return R.success(setmealList);
}

9.3 购物车

9.3.1 需求分析

移动端用户可以将菜品或者套餐添加到购物车。
对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车;
对于套餐来说,可以直接点击十将当前套餐加入购物车。
在购物车中可以修改菜品和套餐的数量,也可以清空购物车。

9.3.2 数据模型

购物车对应的数据表为shopping_cart表
瑞吉外卖项目----(1)基本功能实现_第63张图片

9.3.3 代码开发

梳理一下购物车操作时前端页面和服务端的交互过程:
1、点击加入购物车或者 + 按钮,页面发送ajax请求,请求服务端,将菜品或者套餐添加到购物车
瑞吉外卖项目----(1)基本功能实现_第64张图片
瑞吉外卖项目----(1)基本功能实现_第65张图片
瑞吉外卖项目----(1)基本功能实现_第66张图片
2、点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐
3、点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作

开发购物车功能,其实就是在服务端编写代码去处理前端页面发送的这3次请求即可。

在开发业务功能前,先将需要用到的类和接口基本结构创建好:
● 实体类ShoppingCart (直接从课程资料中导入即可)
● Mapper接口 ShoppingCartMapper
● 业务层接口ShoppingCartService;业务层实现类ShoppingCartServicelmpl
● 控制层ShoppingCartController

代码开发:

注意:不能直接添加购物车,要先查询此东西是否已加入购物车,如果已加入amount加1, 未加入则加入。

/**
 * 添加购物车
 * @param shoppingCart
 * @return
 */
@PostMapping("/add")
public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
    log.info("购物车数据:{}",shoppingCart);
    //设置用户id,指定当前是哪个用户的购物车数据
    Long currentId = BaseContext.getCurrentId();
    shoppingCart.setUserId(currentId);
    //查询当前菜品或者套餐是否在购物车中
    //判断添加的是菜品还是套餐
    Long dishId = shoppingCart.getDishId();
    LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
    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 cartServiceOne = shoppingCartService.getOne(queryWrapper);
    if(cartServiceOne != null){
        //如果已经存在,就在原来数量基础上加一
        Integer number = cartServiceOne.getNumber();
        cartServiceOne.setNumber(number + 1);
        shoppingCartService.updateById(cartServiceOne);
    }else{
        //如果不存在,则添加到购物车,数量默认就是一
        shoppingCart.setNumber(1);//也可以不写,数据库默认1
        shoppingCart.setCreateTime(LocalDateTime.now());  //添加加入菜品的时间,方便后面购物车排序
        shoppingCartService.save(shoppingCart);
        cartServiceOne = shoppingCart;
    }
    return R.success(cartServiceOne);
}

获取购物车数据:
首先将之前修改的前端代码修改回来
瑞吉外卖项目----(1)基本功能实现_第67张图片

/**
 * 查看购物车
 * @return
 */
@GetMapping("/list")
public R<List<ShoppingCart>> list(){
    log.info("查看购物车...");
    LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
    queryWrapper.orderByDesc(ShoppingCart::getCreateTime);   //根据添加到购物车的时间升序排列
    List<ShoppingCart> list = shoppingCartService.list(queryWrapper);
    return R.success(list);
}

清空购物车:
在这里插入图片描述

/**
 * 清空购物车
 * @return
 */
@DeleteMapping("/clean")
public R<String> clean(){
    //SQL:delete from shopping_cart where user_id = ?
    LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>()
    queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
    shoppingCartService.remove(queryWrapper);
    return R.success("清空购物车成功!");
}

9.4 用户下单

9.4.1 需求分析

移动端用户将菜品或者套餐加入购物车后,可以点击购物车中的去结算按钮,页面跳转到订单确认页面,点击去支付按钮则完成下单操作。

9.4.1 数据模型

用户下单业务对应的数据表为orders表和order_ detail表:
● orders:订单表
瑞吉外卖项目----(1)基本功能实现_第68张图片
● order_ detail:订单明细表
瑞吉外卖项目----(1)基本功能实现_第69张图片

9.4.1 代码开发

在开发代码之前,需要梳理一下用户下单操作时前端页面和服务端的交互过程:
1、在购物车中点击[去结算]按钮, 页面跳转到订单确认页面
瑞吉外卖项目----(1)基本功能实现_第70张图片

2、在订单确认页面,发送ajax请求, 请求服务端获取当前登录用户的默认地址
瑞吉外卖项目----(1)基本功能实现_第71张图片

3、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的购物车数据
瑞吉外卖项目----(1)基本功能实现_第72张图片

4、在订单确认页面点击[去支付]按钮,发送ajax请求, 请求服务端完成下单操作
瑞吉外卖项目----(1)基本功能实现_第73张图片

开发用户下单功能,其实就是在服务端编写代码去处理前端页面发送的请求即可。

在开发业务功能前,先将需要用到的类和接口基本结构创建好:
● 实体类 Orders、OrderDetail (直接从课程资料中导入即可)
● Mapper接口 OrdersMapper、OrderDetailMapper
● 业务层接口OrdersService、OrderDetailService
● 业务层实现类OrdersServicelmpl、OrderDetailServicelmpl
● 控制层OrdersController、 OrderDetailController

代码实现:

下单不需要传套菜信息,因为可以通过线程里用户id从购物车数据库查。外卖项目每次购买方法都是提交购物车,不需要像商城那样购物车存一堆不买的东西。
下单的难点是既要保存订单信息,还要保存订单详情页,所以要新写个submit业务方法。
使用mp的IdWorker.getId()生成雪花算法的订单id。
查询当前用户购物车数据后要遍历计算总金额,总金额类型AtomicInteger原子整型,可以保证线程安全。addAndGet是先加再获取,getAndAdd是先获取再加。

@Service
public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements OrdersService {
    @Autowired
    private ShoppingCartService shoppingCartService;
    @Autowired
    private UserService userService;
    @Autowired
    private AddressBookService addressBookService;
    @Autowired
    private OrderDetailService orderDetailService;
    /**
     * 用户下单
     * @param orders
     */
    @Override
    @Transactional
    public void submit(Orders orders) {
        //获得当前用户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);

        //查询地址数据
        Long addressBookId = orders.getAddressBookId();
        AddressBook addressBook = addressBookService.getById(addressBookId);
        if(addressBook == null){
            throw new CustomException("用户地址信息有误,不能下单");
        }
        //用IdWorker设置订单号
        long orderId = IdWorker.getId();//订单号
        //AtomicInteger原子整型,可以保证线程安全。addAndGet是先加再获取,getAndAdd是先获取再加。
        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());
            //叠加金额,对于不需要任何准确计算精度的数字可以直接使用float或double,但是如果需要精确计算的结果,则必须使用BigDecimal类,而且使用BigDecimal类也可以进行大数的操作。
            //BigDecimal的intValue()方法把BigDecimal类型转为int型。addAndGet相当于x=x+y
            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(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()));
        //向订单表插入数据,一条数据
        this.save(orders);

        //向订单明细表插入数据,多条数据
        orderDetailService.saveBatch(orderDetails);

        //清空购物车数据
        shoppingCartService.remove(wrapper);
    }
}

BigDecimal和double区别:和对于不需要任何准确计算精度的数字可以直接使用float或double,但是如果需要精确计算的结果,则必须使用BigDecimal类,而且使用BigDecimal类也可以进行大数的操作。 intValue()方法把BigDecimal类型转为int型

/**
 * 用户下单
 * @param orders
 * @return
 */
@PostMapping("/submit")
public R<String> submit(@RequestBody Orders orders){
    log.info("订单数据:{}",orders);
    ordersService.submit(orders);
    return R.success("下单成功!!");
}

参考文章

你可能感兴趣的:(瑞吉外卖项目,瑞吉外卖,springboot)