瑞吉外卖【后台管理系统篇】

2年前 (2022) 程序员胖胖胖虎阿
427 0 0

瑞吉外卖

  • 一、软件开发整体介绍
    • 1.软件开发流程
    • 2.角色分工
    • 3.软件环境
  • 二、瑞吉外卖项目介绍
    • 1.项目介绍
    • 2.技术选型
    • 3.功能架构
  • 三、开发环境搭建
    • 1.数据库环境搭建
    • 2.maven项目搭建
  • 四、后台功能开发
    • 1.员工管理
      • 1.1 后台登录功能开发
        • 1.1.1需求开发
        • 1.1.2代码开发
      • 1.2 后台退出功能开发
        • 1.2.1 需求分析
        • 1.2.2 代码开发
      • 1.3 完善登录功能
        • 1.3.1 问题分析
        • 1.3.2 代码实现
      • 1.4 新增员工
        • 1.4.1 需求分析
        • 1.4.2 数据模型
        • 1.4.3 代码开发
      • 1.5 员工信息分页查询
        • 1.5.1 需求分析
        • 1.5.2 代码开发
      • 1.6 启用/禁用员工账号
        • 1.6.1 需求分析
        • 1.6.2 代码开发
        • 1.6.3 功能测试
        • 1.6.3 代码修复
      • 1.7 编辑员工信息
        • 1.7.1 需求分析
        • 1.7.2 代码开发
    • 2. 分类管理
      • 2.1 公共字段自动填充
        • 2.1.1 问题分析
        • 2.1.2 代码实现
        • 2.1.3 代码完善
      • 2.2 新增分类
        • 2.2.1 需求分析
        • 2.2.2 数据模型
        • 2.2.3 代码开发
      • 2.3 分类信息分页查询
        • 2.3.1 需求分析
        • 2.3.2 代码开发
        • 2.3.2 报错和问题解决
      • 2.4 删除分类
        • 2.4.1 需求分析
        • 2.4.2 代码开发
        • 2.4.3 功能完善
      • 2.5 修改分类
        • 2.5.1 需求分析
        • 2.5.2 代码开发
    • 3. 菜品管理
      • 3.1 文件上传下载
        • 3.1.1 文件上传介绍
        • 3.1.2 文件下载介绍
        • 3.1.3 文件上传代码实现
        • 3.1.4 文件下载代码实现
      • 3.2 新增菜品
        • 3.2.1 需求分析
        • 3.2.2 数据模型
        • 3.2.3 代码开发
      • 3.3 菜品信息分页查询
        • 3.3.1 需求分析
        • 3.3.2 代码开发
      • 3.4 修改菜品
        • 3.4.1 需求分析
        • 3.4.2 代码开发
      • 3.5 批量停售起售
        • 3.5.1 需求分析
        • 3.5.1 代码开发
      • 3.6 批量删除
        • 3.6.1 需求分析
        • 3.6.2 代码开发
    • 4. 套餐管理
      • 4.1 新增套餐
        • 4.1.1 需求分析
        • 4.1.2 数据模型
        • 4.1.3 代码开发
      • 4.2 套餐信息分页查询
        • 4.2.1 需求分析
        • 4.2.2 代码开发
      • 4.3 删除套餐
        • 4.3.1 需求分析
        • 4.3.2 代码开发
      • 4.4 修改套餐
        • 4.4.1 需求分析
        • 4.4.2 代码开发
      • 4.5 批量起售停售
        • 4.5.1 需求分析
        • 4.5.2 代码开发
    • 5. 订单明细
      • 5.1 需求分析
      • 5.2 代码开发

黑马平台瑞吉外卖学习地址

一、软件开发整体介绍

1.软件开发流程

瑞吉外卖【后台管理系统篇】

2.角色分工

  • 项目经理:对整个项目负责,任务分配、把控进度
  • 产品经理:进行需求调研,输出需求调研文档、产品原型等
  • UI设计师:根据产品原型输出界面效果图
  • 架构师:项目整体架构设计、技术选型等
  • 开发工程师:代码的实现
  • 测试工程师:编写测试用例,输出测试报告
  • 运维工程师:软件环境搭建、项目上线

3.软件环境

  • 开发环境(development):开发人员在开发阶段使用的环境,一般外部用户无法访问
  • 测试环境(testing):专门给测试人员使用的环境,用于测试项,一般外部用户无法访问
  • 生产环境(production):即线上环境,正式提供对外服务的环境

二、瑞吉外卖项目介绍

1.项目介绍

为餐饮定制的一款软件,包括系统管理后台和移动端应用两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的菜品、套餐、订单等进行管理维护。移动端应用主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单等。
项目分为三期:
第一期实现基本需求,移动端通过H5实现,用户可以通过手机浏览器访问。
第二期主要针对移动端应用进行改进,使用微信小程序实现,用户用起来更方便加方便。
第三期主要针对系统进行优化升级,提供系统的访问性能。

  • 产品原型
    产品原型,就是一款产品成型之前的一个简单的框架,就是将页面的排版布局展现出来,使产品的初步构思有一个可视化的展示。通过原型展示,可以更加直观的了解项目的需求和提供的功能。

2.技术选型

瑞吉外卖【后台管理系统篇】

3.功能架构

瑞吉外卖【后台管理系统篇】

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

三、开发环境搭建

1.数据库环境搭建

创建项目对应的数据库(图形界面或者命令行都可以)
瑞吉外卖【后台管理系统篇】
使用CMD命令进入MySQL,bin目录下,执行命令

mysql -hlocalhost -uroot -p

输入登录密码

创建数据库:

create database reggie character set uft8mb4;

使用图形工具导入SQL文件:
瑞吉外卖【后台管理系统篇】

使用命令行导入数据库

source SQL地址(地址不可以放在中文路径下)

数据表
瑞吉外卖【后台管理系统篇】

2.maven项目搭建

1)创建maven项目
瑞吉外卖【后台管理系统篇】
创建完项目后,检查项目的编码、Maven仓库配置、Jdk配置等
瑞吉外卖【后台管理系统篇】
瑞吉外卖【后台管理系统篇】
瑞吉外卖【后台管理系统篇】
瑞吉外卖【后台管理系统篇】
2)导入pom文件相关技术依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
    </parent>
    <groupId>com.reggie</groupId>
    <artifactId>reggie_take_out</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>


    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>

        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.23</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.4.5</version>
            </plugin>
        </plugins>
    </build>
</project>

3)创建配置文件application.yml

server:
  port: 8080  # tomcat端口号
spring:
  application:
    name: reggie_take_out # 服务的名称
  datasource:             # 数据源
    druid:
      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: root
mybatis-plus:
  configuration:
    # address_book --> AddressBook
    #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: ASSIGN_ID    # 主键

4)编写启动类

package com.reggie;


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

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

5)映射静态资源
前端资源放在resource目录下
创建配置类映射前端资源

@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {


    /**
     * 设置静态资源映射
     *
     * @param registry
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("开始静态资源映射...");
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
    }
}

四、后台功能开发

1.员工管理

1.1 后台登录功能开发

1.1.1需求开发

1)页面原型
瑞吉外卖【后台管理系统篇】
2)登录页面展示
瑞吉外卖【后台管理系统篇】
3)查看登录请求信息
通过浏览器调试工具F12,可以发现,点击登录按钮时,页面会发送请求(请求地址为http://localhost:8080/emplpyee/login)并提交参数(username和password),报404,因为后台系统没有响应次请求的处理器,所以需要创建相关类来处理登录请求
瑞吉外卖【后台管理系统篇】
瑞吉外卖【后台管理系统篇】

4)数据模型(employee)表
瑞吉外卖【后台管理系统篇】

1.1.2代码开发

1)创建一个实体类Employee,和employee表进行映射

/**
 * 员工实体类
 */
@Data
public class Employee implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    private String username;

    private String name;

    private String password;

    private String phone;

    private String sex;

    private String idNumber;    //身份证号码

    private Integer status;

    private LocalDateTime createTime;

    private LocalDateTime updateTime;

    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;

}

2)创建Controller、Servcie、Mapper
瑞吉外卖【后台管理系统篇】

  1. Controller
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
    @Autowired
    private EmployeeService employeeService;
}

  1. Service
public interface EmployeeService extends IService<Employee> {
}
  1. Service实现类
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
}
  1. Mapper
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {
}

3)编写返回结果类R
此类是一个通用结果类,服务端响应结果最终都会包装成此种类型返回给前端页面

/**
 * 通用返回结果类,服务端响应的数据最终都会封装为此对象
 * @param <T>
 */
@Data
public class R<T> {

    private Integer code; //编码:1成功,0和其它数字为失败

    private String msg; //错误信息

    private T data; //数据

    private Map map = new HashMap(); //动态数据

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

}

4)在Controller中创建登录方法
处理逻辑如下:
1、将页面提交的密码password进行md加密处理
2、根据页面提交的用户名username查询数据库
3、如果没有查询到则返回登录失败结果
4、密码对比,如果不一致则返回登录失败结果
5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
6、登录成功,将员工id存入Session并返回登录成功结果
瑞吉外卖【后台管理系统篇】

    @Autowired
    private EmployeeService employeeService;

    /**
     * 员工登录
     *
     * @param request
     * @param employee
     * @return
     */
    @PostMapping("/login")
    public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee) {

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

        //2.根据页面提交的用户名username查询数据库
        LambdaQueryWrapper<Employee> qureeyWrapper = new LambdaQueryWrapper<>();
        qureeyWrapper.eq(Employee::getUsername, employee.getUsername());
        Employee emp = employeeService.getOne(qureeyWrapper);

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

1.2 后台退出功能开发

1.2.1 需求分析

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

1.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("退出成功");
    }

1.3 完善登录功能

1.3.1 问题分析

前面已经完成了后台登录的员工登录功能开发,但是还存在一个问题,如果用户不登录,直接访问系统首页面,照样可以访问。
这种设计不合理,我们希望看到的效果应该是,只有登录成功后才可以访问系统中的页面,如果没有登录则跳转到登录页面。

可以使用过滤器或者拦截器,在过滤器或者拦截器中判断用户是否已经完成登录,如果没有登录则跳转到登录页面。

1.3.2 代码实现

实现步骤:
1、创建自定义过滤器LoginCheckFilter

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

	}
}

2、在启动类上加入注解@ServletComponentScan

@Slf4j
@SpringBootApplication
@ServletComponentScan
public class ReggieApplication {
    public static void main(String[] args) {
        SpringApplication.run(ReggieApplication.class, args);
        log.info("项目启动成功");
    }
}

3、完善过滤器的处理逻辑

过滤器具体的处理逻辑如下:

    /**
     * 路径匹配,检查本次请求是否需要放行
     *
     * @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 == true) {
                return true;
            }
        }
        return false;
    }

1、获取本次请求的URI

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

        //定义不需要处理的请求路径
        String[] urls = new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**"
        };

2、判断本次请求是否需要处理

        boolean check = check(urls, requestURI);

3、如果不需要处理,则直接放行

        //3、如果不需要处理,则直接放行
        if (check) {
            filterChain.doFilter(request, response);
            return;
        }

4、判断登录状态,如果已经登录,则直接放行

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

5、如果未登录则返回未登录结果

在这里插入代码片        //5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));

        return;

瑞吉外卖【后台管理系统篇】

1.4 新增员工

1.4.1 需求分析

后台系统中可以管理员工信息,通过新增员工来添加后台系统用户,点击添加用户跳转到新增页面
瑞吉外卖【后台管理系统篇】

1.4.2 数据模型

新增员工,其实就是将新增页面录入的员工数据插入到employee表。需要注意,employee表中对username字段加了唯一约束,因为username是员工的唯一账号
瑞吉外卖【后台管理系统篇】
employee表中status字段已经设置了默认值1,表示状态正常。
瑞吉外卖【后台管理系统篇】

1.4.3 代码开发

程序的执行过程:
1、页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
2、服务端controller接收页面提交的数据并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据
瑞吉外卖【后台管理系统篇】

    /**
     * 新增员工
     *
     * @param employee
     * @return
     */
    @PostMapping
    public R<String> save(HttpServletRequest request, @RequestBody Employee employee) {
        log.info("新增员工,员工信息:{}", employee.toString());
        //设置初始密码为,进行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);

        employeeService.save(employee);

        return R.success("新增员工成功");
    }

前面的程序还存在一个BUG,当我们新增员工的时候输入的账号已经存在了,由于employee表中对该字段加入了唯一约束,此时会抛出异常

java.sql.SQLIntegrityConstraintViolationException: Duplicate entry ‘zhangsan’ for key ‘employee.idx_username’

此时需要对程序进行异常捕获,通常有两种处理方式:
1、在Controller方法中加入try、catch进行异常捕获

        try {
            employeeService.save(employee);
        } catch (Exception e) {
            R.error("新增员工失败");
        }

2、使用异常处理器进行全局异常捕获

/**
 * 全局异常处理器
 */
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j

public class GlobaExceptionHandler {
    /**
     * 异常处理方法
     *
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) {
        log.error(ex.getMessage());
        if (ex.getMessage().contains("Duplicate entry")) {
            String[] split = ex.getMessage().split(" ");
            String msg = split[2] + "已存在";
            return R.error(msg);
        }

        return R.error("未知错误");
    }
}

总结:
1、根据产品原型明确业务需求
2、重点分析数据的流转过程和数据格式
3、通过debug断点调试跟踪程序执行过程
瑞吉外卖【后台管理系统篇】

1.5 员工信息分页查询

1.5.1 需求分析

系统中的员工很多的时候,如果在一个页面中展示出来会显得很乱,不便于查看,所以一般的系统中都会以分页的形式来展示列表数据。
瑞吉外卖【后台管理系统篇】

1.5.2 代码开发

配置MabitsPlus的分页插件

/**
 * 配置MP的分页插件
 */
@Configuration
public class MybatisPlusConfig {

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

程序的执行过程:
1、页面发送ajax请求,将分页查询参数(page、pageSize、name)
2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面
5、页面接收到分页数据并通过ElementUI的Table组件展示到页面上
瑞吉外卖【后台管理系统篇】
瑞吉外卖【后台管理系统篇】

    /**
     * 员工信息的分页查询
     *
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @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();

        //添加一个过滤条件 (当name不为空是添加条件)
        queryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);
        //添加一个排序条件
        queryWrapper.orderByDesc(Employee::getUpdateTime);

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

1.6 启用/禁用员工账号

1.6.1 需求分析

在员工管理列表页面,对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。
需要注意的是,只有管理员(admin)用户可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示。
瑞吉外卖【后台管理系统篇】
页面中的ajax请求发送
瑞吉外卖【后台管理系统篇】

1.6.2 代码开发

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

    /**
     * 根据id来修改员工信息
     *
     * @param employee
     * @return
     */
    @PutMapping

    public R<String> update(HttpServletRequest request, @RequestBody Employee employee) {
        log.info(employee.toString());
        Long empId = (Long) request.getSession().getAttribute("employee");
        employee.setUpdateTime(LocalDateTime.now());
        employee.setUpdateUser(empId);
        employeeService.updateById(employee);
        return R.success("员工信息修改成功");
    }

1.6.3 功能测试

测试过程中没有报错,但是功能没有实现,查看数据库也没有变化。观察控制台输出的SQL:
瑞吉外卖【后台管理系统篇】
SQL执行结果更新的数据行为0,仔细观察id,和数据库中的id并不对应
瑞吉外卖【后台管理系统篇】

1.6.3 代码修复

通过观察控制台id与SQL数据库id不一致的原因:分页查询时服务端响应给页面的数据id中id的值为19位数字,类型为long
瑞吉外卖【后台管理系统篇】
页面中js处理long型数字只能精确到前16位,所以最终通过ajax请求提交给服务端的时候id变为了1524243373520158700
瑞吉外卖【后台管理系统篇】
发现问题的原因:即js对long型数据进行处理时丢失精度,导致提交的id和数据库的id不一致。

解决问题:可以在服务端给页面响应json数据时进行处理,将long型数据统一转换位String字符串,效果如下:
瑞吉外卖【后台管理系统篇】

具体实现步骤:
1) 提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 */
public class JacksonObjectMapper extends ObjectMapper {

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);


        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

                .addSerializer(BigInteger.class, ToStringSerializer.instance)
                .addSerializer(Long.class, ToStringSerializer.instance)
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

2) 在WebMvcConfig配置类中扩展Spring MVC的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换

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

1.7 编辑员工信息

1.7.1 需求分析

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

1.7.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、页面接收到服务端响应信息后进行相应处理
瑞吉外卖【后台管理系统篇】

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

2. 分类管理

2.1 公共字段自动填充

2.1.1 问题分析

上面已经完成了后台系统的员工管理功能开发,在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间和修改人等字段。这些字段属于公共字段,也就是很多表中都有这些字段,如下:
瑞吉外卖【后台管理系统篇】
瑞吉外卖【后台管理系统篇】
可以使用Mybatis Plus提供的公共字段自动填充功能,对于这些公共字段在某个地方统一处理,简化开发。

2.1.2 代码实现

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

实现步骤:
1、在实体类的属性上加入@TableField注解,指定自动填充的策略

    @TableField(fill = FieldFill.INSERT)    //插入时填充字段
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段
    private LocalDateTime updateTime;

    @TableField(fill = FieldFill.INSERT)        //插入时填充字段
    private Long createUser;

    @TableField(fill = FieldFill.INSERT_UPDATE)     //插入和更新时填充字段
    private Long updateUser;

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

/**
 * 自定义元数据处理器
 */
@Component
@Slf4j
public class MyMetaObjecthand 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", new Long(1));
        metaObject.setValue("updateUser", new Long(1));
    }

    /**
     * 更新操作,自动填充
     *
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("公共字段自动填充[update]...");
        log.info(metaObject.toString());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("updateUser", new Long(1));
    }
}

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

2.1.3 代码完善

上面已经完成了公共字段自动填充功能的代码开发,但还有一个问题没有解决,是我们在自动填充createUser和updateUser时设置的用户id为固定值,我们需要改造成动态获取当前登录用户的id。
注意,我们在MyMetaObjectHandler类中是不能获取HttpSession对象的,所以需要使用其他方式来获取登录用户的id

这里使用ThreadLocal来解决此问题,它时JDK中提供的一个类。

在学习ThreadLocal之前,还要确认一个事,就是客户端发送的每次Http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:
1、LoginCheckFilter的doFilter方法
2、EmployeeController的update方法
3、MyMetaObjectHandler的updateFill方法
可以在上面三个方法中分别加入下面代码(获取当前线程id):

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

执行编辑员工页面进行验证,观察IDEA控制台输出可以看见,一次请求中对应的线程id是相同的:
瑞吉外卖【后台管理系统篇】
ThreadLocal介绍
ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量的时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。

ThreadLocal常用方法

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

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

实现步骤
1、编写BaseContext工具类,基于ThreadLocal封装的工具类

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

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

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

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

BaseContext.setCurrentId(empId);

3、在MyMetaObjectHandler的方法中调用BaseContext获取登录用户的id

        metaObject.setValue("createUser", BaseContext.getCurrentId());
        metaObject.setValue("updateUser", BaseContext.getCurrentId());

2.2 新增分类

2.2.1 需求分析

后台系统中可以管理分类信息,分类包括两种类型,分别是菜品分类和套餐分类。当从后台系统中添加菜品时需要选择一个菜品分类,当在后台系统中添加一个套餐时需要选择一个套餐分类,在移动端也会按照菜品分类和套餐分类来展示对应的菜品和套餐。
瑞吉外卖【后台管理系统篇】
瑞吉外卖【后台管理系统篇】
可以在后台系统的分类管理页面分别添加菜品分类和套餐分类,如下:
瑞吉外卖【后台管理系统篇】瑞吉外卖【后台管理系统篇】

2.2.2 数据模型

新增分类,其实就是将我们新增窗口录入的分类数据插入到category表,结构如下:
瑞吉外卖【后台管理系统篇】

2.2.3 代码开发

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

  • 实体类Category
/**
 * 分类
 */
@Data
public class Category implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;


    //类型 1 菜品分类 2 套餐分类
    private Integer type;


    //分类名称
    private String name;


    //顺序
    private Integer sort;


    //创建时间
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;


    //更新时间
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;


    //创建人
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;


    //修改人
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;


    //是否删除
    @TableField(exist = false)
    private Integer isDeleted;

}
  • Mapper接口CategoryMapper
@Mapper
public interface CategoryMapper extends BaseMapper<Category> {
}
  • 业务层接口CategoryService
public interface CategoryService extends IService<Category> {
}
  • 业务层实现类CategoryServiceImp
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
}
  • 控制层CategoryController
/**
 * 分类管理
 */
@RestController
@Slf4j
@RequestMapping("/category")
public class CategoryController {
    @Autowired
    private CategoryService categoryService;
}

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

可以看到新增菜品分类和新增套餐分类请求的服务端地址和提交的json数据结构相同,所以服务端只需要提供一个方法统一处理即可:
瑞吉外卖【后台管理系统篇】

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

2.3 分类信息分页查询

2.3.1 需求分析

系统中的分类很多的时候,如果在一个页面展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
瑞吉外卖【后台管理系统篇】

2.3.2 代码开发

程序的执行过程:
1、页面发送ajax请求,将分页查询参数(page、pageSize)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面
5、页面接收到分页数据通过ElementUI的Table组件展示到页面上
瑞吉外卖【后台管理系统篇】

    /**
     * 分页查询
     *
     * @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.orderByDesc(Category::getSort);

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

2.3.2 报错和问题解决

服务器内部错误
瑞吉外卖【后台管理系统篇】
查看IDEA控制台输出
瑞吉外卖【后台管理系统篇】

### Error querying database. Cause: java.sql.SQLSyntaxErrorException: Unknown column 'is_deleted' in 'field list'
报这个错一般出现在新增字段或者修改字段,以及操作连表的时候

  1. 可能左链接或内连接查询时用的字段名称和已有的字段名称重复
  2. 可能某个字段数据库与实体类有区别,而SQL中没有对应上,检查数据库和SQL的字段
  3. 可能在连表查的时候,某些字段所在的库错了,比如要查user的 u.name却写成了team的t.name

另外如果有这种特殊需求:实体类需要加入某个字段,但数据库对应的表中并不需要该字段,次时可以在实体类中,给该字段加上@TableField(exist = false)这个注解,这样实体类在构建时会含有这个字段,但并不会加到数据库

在Category实体类里的 isDeleted 字段数据库中并没有
瑞吉外卖【后台管理系统篇】
瑞吉外卖【后台管理系统篇】给该字段加上@TableField(exist = false)这个注解
瑞吉外卖【后台管理系统篇】
重启项目,再次访问网页,此时成功访问
瑞吉外卖【后台管理系统篇】

2.4 删除分类

2.4.1 需求分析

在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。
瑞吉外卖【后台管理系统篇】

2.4.2 代码开发

程序的执行过程:
1、页面发送ajax请求,将参数(id)提交到服务端
2、服务端Controller接收页面提交的数据并调用Servcie删除数据
3、Servcie调用Mapper操作数据库
瑞吉外卖【后台管理系统篇】

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

        categoryService.removeById(ids);
        return R.success("分类信息删除成功");
    }

2.4.3 功能完善

上面已经实现了根据ids删除分类的功能,但并没有检查删除的分类是否关联了菜品或者套餐,所以需要进行功能完善
完善分类删除功能,需要准备基础的接口和类:
1、实体类Dish和Setmeal

//Dish
/**
 菜品
 */
@Data
public class Dish implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;


    //菜品名称
    private String name;


    //菜品分类id
    private Long categoryId;


    //菜品价格
    private BigDecimal price;


    //商品码
    private String code;


    //图片
    private String image;


    //描述信息
    private String description;


    //0 停售 1 起售
    private Integer status;


    //顺序
    private Integer sort;


    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;


    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;


    @TableField(fill = FieldFill.INSERT)
    private Long createUser;


    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;


    //是否删除
    private Integer isDeleted;

}

//Setmeal
/**
 * 套餐
 */
@Data
public class Setmeal implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;


    //分类id
    private Long categoryId;


    //套餐名称
    private String name;


    //套餐价格
    private BigDecimal price;


    //状态 0:停用 1:启用
    private Integer status;


    //编码
    private String code;


    //描述信息
    private String description;


    //图片
    private String image;


    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;


    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;


    @TableField(fill = FieldFill.INSERT)
    private Long createUser;


    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;


    //是否删除
    private Integer isDeleted;
}

2、Mapper接口DishMapper和SetmealMapper

//DishMapper
@Mapper
public interface DishMapper extends BaseMapper<DishMapper> {
}
//SetmealMapper
@Mapper
public interface SetmealMapper extends BaseMapper<Setmeal> {
}

3、Service接口DishService和SetmealService

//DishService
public interface DishService extends IService<Dish> {
}
//SetmealService
public interface SetmealService extends IService<Setmeal> {
}

4、Service实现类DishServiceImpl和SetmealServiceImpl

//DishServiceImpl
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
}
//SetmealServiceImpl
@Service
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService {
}

5、创建自定义业务异常类处理异常

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

6、在全局异常处理器中添加异常处理方法

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

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

7、在CategoryServiceImpl实现类中实现功能

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

    @Autowired
    private DishService dishService;

    @Autowired
    private SetmealService setmealService;

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

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

2.5 修改分类

2.5.1 需求分析

在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作
瑞吉外卖【后台管理系统篇】

2.5.2 代码开发

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

        categoryService.updateById(category);
        
        return R.success("修改分类信息成功");
    }

3. 菜品管理

瑞吉外卖【后台管理系统篇】

3.1 文件上传下载

3.1.1 文件上传介绍

文件上传(upload),指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。经常在发微博,发微信朋友圈都用用到了文件上传功能。

文件上传时,对页面的form表单有如下要求:

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

示例:
< from method = "post" action=“/common/upload” enctype="multipart/form-data">
         <input name = “myfile” type = "file" />
         <input name = “submit” type = "提交" />
< /from >

前端的一些组件库提供了对应的上传组件,但是底层原理还是基于form表单的文件上传。如:ElementUI中提供的upload上传组件:
瑞吉外卖【后台管理系统篇】
服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件:

  • commons-fileupload
  • commons-io

Spring框架在Spring-web包中对文件上传进行了封装,简化了服务端代码,只需要在controller的方法中声明一个MultipartFile类型 参数即可接收上传文件,例如:
瑞吉外卖【后台管理系统篇】

3.1.2 文件下载介绍

文件下载(download),指将文件从服务器传输到本地计算机的过程。

通过浏览器进行文件下载,通常有两种表现形式:

  • 以附件的形式下载,弹出保存对话框,将文件保存到指定磁盘目录
  • 直接在浏览器中打开

通过浏览器进行文件下载,本质上是服务端将文件以流的形式写回浏览器的过程。

3.1.3 文件上传代码实现

文件上传,页面端可以使用ElementUI提供的上传组件。

        <el-upload class="avatar-uploader"
                action="/common/upload"
                :show-file-list="false"
                :on-success="handleAvatarSuccess"
                :before-upload="beforeUpload"
                ref="upload">
            <img v-if="imageUrl" :src="imageUrl" class="avatar"></img>
            <i v-else class="el-icon-plus avatar-uploader-icon"></i>
        </el-upload>

在LoginCheckFilter拦截器里添加不需要拦截的请求路径,以便文件上传

        //定义不需要处理的请求路径
        String[] urls = new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**",
                "/common/**"
        };

在application.yml配置定义路径,存放上传的图片

reggie:
  path: D:\img\

创建一个控制器CommonController

/**
 * 文件的上传和下载
 */
@RestController
@RequestMapping("/common/")
@Slf4j
public class CommonController {

    @Value("${reggie.path}")
    private String basePath;

    /**
     * 文件上传
     *
     * @param file
     * @return
     */
    @PostMapping("/upload")
    //参数的名字不能随便起,要和Form Data里的name保持一致,否则是不能接收的
    public R<String> upload(MultipartFile file) {
        //file是一个临时文件需要转存到指定位置,否则本次请求完成后临时文件会删除
        log.info(file.toString());

        //原始文件名
        String originalFilename = file.getOriginalFilename();
        String siffix = originalFilename.substring(originalFilename.lastIndexOf("."));
        //使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
        String fileName = UUID.randomUUID().toString() + siffix;

        //创建一个目录对象
        File dir = new File(basePath);
        //判断当前目录是否存在
        if (!dir.exists()) {
            //目录不存在需要创建
            dir.mkdir();
        }

        try {
            //将临时文件转存到指定位置
            file.transferTo(new File(basePath + fileName));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return R.success(fileName);
    }
}

3.1.4 文件下载代码实现

文件下载,页面端可以使用< img >标签展示下载的图片

<img v-if ="imageUrl" :src="imageUrl" class ="avatar" />
handleAvatarSuccess(response,file,fileList){
	this.imageUrl = '/common/download?name=${response.data}'
},

代码实现:

    /**
     * 文件下载
     *
     * @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/jpeg");

            byte[] bytes = new byte[1024];
            int len = 0;
            while ((len = fileInputStream.read(bytes)) != -1) {
                outputStream.write(bytes, 0, len);
                outputStream.flush();
            }
            //关闭资源
            outputStream.close();
            fileInputStream.close();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

3.2 新增菜品

3.2.1 需求分析

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

3.2.2 数据模型

新增菜品,就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。

所以在新增菜品的时候,涉及到了两个表:

  • dish菜品表
    瑞吉外卖【后台管理系统篇】

  • dish_flavor菜品口味表
    瑞吉外卖【后台管理系统篇】

3.2.3 代码开发

开发前先将用到的类和接口基本结构创建:

  • 实体类DishFlavor
/**
菜品口味
 */
@Data
public class DishFlavor implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;


    //菜品id
    private Long dishId;


    //口味名称
    private String name;


    //口味数据list
    private String value;


    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;


    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;


    @TableField(fill = FieldFill.INSERT)
    private Long createUser;


    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;


    //是否删除
    private Integer isDeleted;

}

  • Mapper接口,DishFlavorMapper
@Mapper
public interface DishFlavorMapper extends BaseMapper<DishFlavorMapper>  {
}
  • 业务层接口,DishFlavorService
public interface DishFlavorService extends IService<DishFlavor>  {
}
  • 业务层实现类,DishFlavorServiceImpl
@Service
public class DishFlavorServiceImpl extends ServiceImpl<DishFlavorMapper, DishFlavor> implements DishFlavorService {
}
  • 控制层DishController
  /**
 * 菜品管理
 */
@RestController
@RequestMapping("/dish")
public class DishController {

    @Autowired
    private DishService dishService;

    @Autowired
    private DishFlavorService dishFlavorService;
}

了解新增菜品时前端页面和服务端的交互过程:
1、页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中

瑞吉外卖【后台管理系统篇】
可以看到该功能是写在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).orderByAsc(Category::getUpdateTime);

        List<Category> list = categoryService.list(queryWrapper);
        return R.success(list);
    }

2、页面发送请求进行图片上传,请求服务器端将图片保存到服务器
3、页面发送请求进行图片下载,将上传的图片进行回显(直接使用上边开发的CommonController )
4、点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端
瑞吉外卖【后台管理系统篇】在DishController控制层实现该功能:

注意事项:
DTO,全称Data Transfer Object,即数据传输对象,一般用于展示层与服务层之间的数据传输。
分析:
前面没有使用到DTO是因为,前面的传输数据和实体类的属性都是一一对应的,现在传输的数据跟实体类里的属性并不是一一对应的,这时候就需要专门的DTO来传输。
新建:
这里创建一个DTO包建一个DishDTO类,这里只用到了List 下面两个后续开发使用。

@Data
public class DishDto extends Dish {

    private List<DishFlavor> flavors = new ArrayList<>();

    private String categoryName;

    private Integer copies;
}

业务实现层,因为多表查询所以添加了事务管理

@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
    @Autowired
    private DishFlavorService dishFlavorService;

    /**
     * 新增菜品同时保存对应的口味数据
     *
     * @param dishDto
     */
    @Override
    @Transactional
    public void saveWithFlavor(DishDto dishDto) {
        //保存菜品的基本信息到菜品表dish
        this.save(dishDto);

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

        List<DishFlavor> flavors = dishDto.getFlavors();    //菜品口味
        flavors = flavors.stream().map((item) -> {
            item.setDishId(dishID);
            return item;
        }).collect(Collectors.toList());

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

在启动类上开启事务功能注解

@EnableTransactionManagement

控制层

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

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

3.3 菜品信息分页查询

3.3.1 需求分析

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

3.3.2 代码开发

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

开发菜品信息分页查询功能,就是在服务端,编写代码去处理前端页面发送的这2次请求即可。

    /**
     * 菜品信息分页
     *
     * @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> dtopageInfo = new Page(page, pageSize);

        //构造条件构造器
        LambdaQueryWrapper<Dish> lambdaQueryWrapper = new LambdaQueryWrapper<>();

        //添加过滤条件
        lambdaQueryWrapper.like(name != null, Dish::getName, name);

        //添加排序条件
        lambdaQueryWrapper.orderByDesc(Dish::getUpdateTime);

        //执行分页查询
        dishService.page(pageInfo, lambdaQueryWrapper);

        //对象拷贝
        BeanUtils.copyProperties(pageInfo, dtopageInfo, "records");
        List<Dish> records = pageInfo.getRecords();
        List<DishDto> list = records.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);
            }

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


        dtopageInfo.setRecords(list);

        return R.success(dtopageInfo);
    }

3.4 修改菜品

3.4.1 需求分析

在菜品管理列表页面点击修改按钮,跳转到修改菜品页面,在修改页面回显菜品相关信息并进行修改,最后点击确定按钮完成修改操作
瑞吉外卖【后台管理系统篇】

3.4.2 代码开发

修改菜品时前端页面(add.html)和服务端的交互过程:
1、页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示(前面已经实现 )
2、页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
DishServiceImpl业务实现层:

    /**
     * 根据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<>();
        //根据菜品的id查对应的口味
        queryWrapper.eq(DishFlavor::getId, dish.getId());

        List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);
        dishDto.setFlavors(flavors);
        return dishDto;
    }

DishController控制层:

    /**
     * 根据id来查询菜品信息和对应的口味信息
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public R<DishDto> get(@PathVariable Long id) {
        DishDto dishDto = dishService.getByIdWithFlavor(id);

        return R.success(dishDto);
    }

3、页面发送ajax请求,请求服务端进行图片下载,用于页面图片回显(前面已经实现 )
4、点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以JSON的形式提交到服务端
DishServiceImpl业务实现层:

    /**
     * 更新菜品信息,同时更新对应的口味信息
     *
     * @param dishDto
     */
    @Transactional
    @Override
    public void updateWithFlavor(DishDto dishDto) {
        //更新dish表基本信息
        this.updateById(dishDto);
        //清理当前菜品对应口味数据--dish_flavor表的delete操作
        LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper();
        queryWrapper.eq(DishFlavor::getId, dishDto.getId());
        dishFlavorService.remove(queryWrapper);

        //添加当前提交过来的口味数据--dish_flavro表的insert操作
        List<DishFlavor> flavors = dishDto.getFlavors();
        flavors = flavors.stream().map((item) -> {
            item.setDishId(dishDto.getId());
            return item;
        }).collect(Collectors.toList());
        dishFlavorService.saveBatch(flavors);
    }

DishController控制层:

    /**
     * 修改菜品
     * @param dishDto
     * @return
     */
    @PutMapping
    public R<String> update(@RequestBody DishDto dishDto) {
        log.info(dishDto.toString());

        dishService.updateWithFlavor(dishDto);

        return R.success("修改菜品成功");
    }

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

3.5 批量停售起售

3.5.1 需求分析

在菜品管理列表页面点击停售按钮,或者勾选菜品名称点击批量停售按,当某些菜品售完后通过修改菜品停售状态,点击确定按钮完成修改操作,以此告之用户使其在用户端禁止下单
瑞吉外卖【后台管理系统篇】

3.5.1 代码开发

起售、停售菜品状态,本质上就是一个更新操作,也就是对status状态字段进行操作,在Controller中创建statusWithIds方法。
先去浏览器查看此次请求的方式和Url路径
瑞吉外卖【后台管理系统篇】
瑞吉外卖【后台管理系统篇】
由上可以观察出此次请求方式都为POST,但是在请求方式后多了一个多了"/status",所以在DishController控制层写方法时要加上/status,而且还传入一个参数0,代表起售停售状态,所以要加上/{status},并通过@PathVariable(“status”) 接收status

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

3.6 批量删除

3.6.1 需求分析

在菜品管理列表页面,可以对某个菜品进行删除操作,和批量删除操作。需要注意的是当前菜品起售状态时,此菜品不允许删除,如果此菜品为停售状态,删除此菜品信息以及此菜品id关联的口味表数据。
瑞吉外卖【后台管理系统篇】

3.6.2 代码开发

程序的执行过程:
1、页面发送ajax请求,将参数(ids)提交到服务端
2、服务端Controller接收页面提交的数据并调用Servcie删除数据
3、Servcie调用Mapper操作数据库
瑞吉外卖【后台管理系统篇】

瑞吉外卖【后台管理系统篇】

    /**
     * 批量删除菜品和单独删除菜品
     *
     * @param ids
     * @return
     */
    @DeleteMapping
    @Transactional  //因为多表操作开启事务
    public R<String> delete(@RequestParam List<Long> ids) {
        //构造一个条件构造器
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        //判断浏览器传来的ids是否为空,并和菜品表中的id进行匹配
        queryWrapper.in(ids != null, Dish::getId,ids);
        List<Dish> list = dishService.list(queryWrapper);
        for (Dish dish : list) {
            //判断当前菜品是否在售卖阶段,0停售,1起售
            if (dish.getStatus() == 0) {
                //停售状态直接删除
                dishService.removeById(dish.getId());
                LambdaQueryWrapper<DishFlavor> dishFlavorLambdaQueryWrapper = new LambdaQueryWrapper<>();
                //根据菜品id匹配口味表中的菜品id
                dishFlavorLambdaQueryWrapper.eq(DishFlavor::getDishId, dish.getId());
                //删除菜品id关联的口味表信息
                dishFlavorService.remove(dishFlavorLambdaQueryWrapper);
            }else {
                throw new CustomException("此菜品还在售卖阶段,删除影响销售!");
            }
        }
        return R.success("删除成功");
    }

4. 套餐管理

瑞吉外卖【后台管理系统篇】
瑞吉外卖【后台管理系统篇】

4.1 新增套餐

4.1.1 需求分析

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

4.1.2 数据模型

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

  • setmeal 套餐表
    瑞吉外卖【后台管理系统篇】

  • setmeal_dish套餐菜品关系表
    瑞吉外卖【后台管理系统篇】

4.1.3 代码开发

在开发之前,先将需要用到的类和接口创建好:

  • 实体类SetmealDish
/**
 * 套餐菜品关系
 */
@Data
public class SetmealDish implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;


    //套餐id
    private Long setmealId;


    //菜品id
    private Long dishId;


    //菜品名称 (冗余字段)
    private String name;

    //菜品原价
    private BigDecimal price;

    //份数
    private Integer copies;


    //排序
    private Integer sort;


    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;


    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;


    @TableField(fill = FieldFill.INSERT)
    private Long createUser;


    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;


    //是否删除
    private Integer isDeleted;
}

  • DTO SetmealDto
@Data
public class SetmealDto extends Setmeal {

    private List<SetmealDish> setmealDishes;

    private String categoryName;
}

  • Mapper接口 SetmealDishMapper
@Mapper
public interface SetmealDishMapper extends BaseMapper<SetmealDish> {
}
  • 业务层接口SetmealDishService
public interface SetmealDishService extends IService<SetmealDish> {
}

  • 业务层实现类SetmealDishServiceImpl
@Service
public class SetmealDishServiceImpl extends ServiceImpl<SetmealDishMapper, SetmealDish> implements SetmealDishService {
}
  • 控制层SetmealController
@Slf4j
@RestController
@RequestMapping("/setmeal")
public class SetmealController {
    @Autowired
    private SetmealService setmealService;

    @Autowired
    private SetmealDishService setmealDishService;
} 

新增套餐时前端页面和服务端的交互过程:
1、页面(backend/page/combo/add.html)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中(上面已经实现)
2、页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中

3、页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
瑞吉外卖【后台管理系统篇】DishController

    /**
     * 根据条件查询对应的菜品数据
     *
     * @param dish
     * @return
     */
    @GetMapping("/list")
    public R<List<Dish>> 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);
        return R.success(list);
    }

4、页面发送请求进行图片上传,请求服务端将图片保存到服务器(上面已经实现)
5、页面发送请求进行图片下载,将上传的图片进行回显(上面已经实现)
6、点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端
瑞吉外卖【后台管理系统篇】
瑞吉外卖【后台管理系统篇】
业务实现层:SetmealServiceImp

    /**
     * 新增套餐,同时保存套餐和菜品的关联关系
     * @param setmealDto
     */
    @Override
    @Transactional  //操作两张表 加入事物
    public void saveWithDish(SetmealDto setmealDto) {
        //保存套餐的基本信息,操作setmeal,执行insert操作
        this.save(setmealDto);
        Long setmealId = setmealDto.getId();    //套餐id

        //保存套餐和菜品的关联关系,操作setmeal_dish,执行insert操作
        List<SetmealDish> dishes = setmealDto.getSetmealDishes();
        dishes.stream().map((item) -> {
            item.setSetmealId(setmealId);
            return item;
        }).collect(Collectors.toList());

        setmealDishService.saveBatch(dishes);
    }

控制层:SetmealController:

    @PostMapping
    public R<String> save(@RequestBody SetmealDto setmealDto) {
        log.info(setmealDto.toString());
        setmealService.saveWithDish(setmealDto);
        return R.success("新增套餐成功");
    }

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

4.2 套餐信息分页查询

4.2.1 需求分析

系统中的套餐数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的形式来展示数据。
瑞吉外卖【后台管理系统篇】

4.2.2 代码开发

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

开发套餐信息分页查询功能,就是在服务端编写代码去处理前端页面发送的这2次请求即可。

    /**
     * 套餐分页查询
     *
     * @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);
    }

4.3 删除套餐

4.3.1 需求分析

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

4.3.2 代码开发

删除套餐时前端页面和服务端的交互过程:
1、删除单个套餐时,页面发送ajax请求,根据套餐id删除对应套餐
瑞吉外卖【后台管理系统篇】
2、删除多个套餐时,页面发送ajax请求,根据提交的多个套餐id删除对应套餐
瑞吉外卖【后台管理系统篇】
开发删除套餐功能,就是在服务端编写代码去处理前端页面发送的这2次请求即可。观察删除单个套餐和批量删除套餐的请求信息可以发现,两种请求的地址和请求方式都是相同的,不同的是传递id的个数,所以在服务端可以提供一个方法来统一处理。

业务实现层SetmealServiceImpl

    /**
     * 删除套餐,同时删除套餐和菜品的关联数据
     *
     * @param ids
     */
    @Override
    @Transactional
    public void removeWithDish(List<Long> ids) {
        //查询套餐的状态,确定是否可以删除
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.in(ids != null, 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
        LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.in(SetmealDish::getSetmealId, ids);

        setmealDishService.remove(lambdaQueryWrapper);
    }

控制层SetmealController

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

4.4 修改套餐

4.4.1 需求分析

在套餐管理列表页面点击修改按钮,跳转到修改套餐页面,在修改页面回显套餐相关信息并进行修改,最后点击确定按钮完成修改操作
瑞吉外卖【后台管理系统篇】

4.4.2 代码开发

修改套餐时前端页面(add.html)和服务端的交互过程:
1、页面发送ajax请求,请求服务端获取分类数据,用于套餐分类下拉框中数据展示(前面已经实现 )
2、页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于套餐信息回显
SetmealServiceImpl业务实现层:

    /**
     * 根据id来查询套餐信息和对应的菜品信息
     *
     * @param id
     * @return
     */
    @Override
    public SetmealDto getByIdWithDish(Long id) {
        //查询套餐的基本信息,从setmeal表中查询
        Setmeal setmeal = this.getById(id);
        SetmealDto setmealDto = new SetmealDto();
        BeanUtils.copyProperties(setmeal, setmealDto);
        //查询当前套餐对应的菜品信息,从setmeal_dish表中查询
        LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
        //根据套餐的id查对应的菜品
        queryWrapper.eq(SetmealDish::getSetmealId,setmeal.getId());

        List<SetmealDish> dishs = setmealDishService.list(queryWrapper);
        setmealDto.setSetmealDishes(dishs);
        return setmealDto;
    }

SetmealController控制层:

    /**
     * 根据id来查询套餐信息和对应的菜品信息
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public R<SetmealDto> get (@PathVariable Long id){
        SetmealDto setmealDao = setmealService.getByIdWithDish(id);
        return R.success(setmealDao);
    }

3、页面发送ajax请求,请求服务端进行图片下载,用于页面图片回显(前面已经实现 )
4、点击保存按钮,页面发送ajax请求,将修改后的套餐相关数据以JSON的形式提交到服务端
SetmealServiceImpl业务实现层:

    /**
     * 更新套餐信息,同时更新对应的菜品信息
     *
     * @param setmealDto
     */
    @Override
    @Transactional
    public void updateWithDish(SetmealDto setmealDto) {
        //更新setmeal表基本信息
        this.updateById(setmealDto);
        //清理当前套餐对应菜品数据--》setmeal_dish表中的delete操作
        LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
        //根据dto的id查对应的菜品信息
        queryWrapper.eq(SetmealDish::getSetmealId, setmealDto.getId());
        setmealDishService.remove(queryWrapper);

        //添加当前提交过来的菜品数据--setmeal_dish表的insert操作
        List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
        setmealDishes.stream().map((item) -> {
            item.setSetmealId(setmealDto.getId());
            return item;
        }).collect(Collectors.toList());
        setmealDishService.saveBatch(setmealDishes);
    }

SetmealController控制层:

    /**
     * 更新套餐信息,同时更新对应的菜品信息
     *
     * @param setmealDto
     */
    @PutMapping
    public R<String> update(@RequestBody SetmealDto setmealDto){
        log.info(setmealDto.toString());

        setmealService.updateWithDish(setmealDto);
        return R.success("修改套餐成功");
    }
}

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

4.5 批量起售停售

4.5.1 需求分析

在套餐管理列表页面点击停售按钮,或者勾选菜品名称点击批量停售按,当某些菜品售完后通过修改菜品停售状态,点击确定按钮完成修改操作,以此告之用户使其在用户端禁止下单

瑞吉外卖【后台管理系统篇】

4.5.2 代码开发

起售、停售菜品状态,本质上就是一个更新操作,也就是对status状态字段进行操作,在Controller中创建statusWithIds方法。
先去浏览器查看此次请求的方式和Url路径
瑞吉外卖【后台管理系统篇】
瑞吉外卖【后台管理系统篇】

由上可以观察出此次请求方式都为POST,但是在请求方式后多了一个多了"/status",所以在SetmealController控制层写方法时要加上/status,而且还传入一个参数0,代表起售停售状态,所以要加上/{status},并通过@PathVariable(“status”) 接收status

     /**
     * 修改套餐的售卖状态
     *
     * @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("售卖状态修改成功");
    }

5. 订单明细

5.1 需求分析

在订单明细页面,可以看移动端用户下单信息,当订单很多的时候,如果在一个页面展示出来会显得比较乱,所以所以一般的系统中都会以分页的方式来展示列表数据。也可以通过订单号,根据时间查询订单。
瑞吉外卖【后台管理系统篇】

5.2 代码开发

程序的执行过程:
1、页面发送ajax请求,将分页查询参数(page、pageSize、number、beginTime、endTime)
2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面
5、页面接收到分页数据并通过ElementUI的Table组件展示到页面上
瑞吉外卖【后台管理系统篇】
瑞吉外卖【后台管理系统篇】

业务实现层:OrderServiceImpl

	/**
     * 后台查询订单明细
     *
     * @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;
    }

控制层:OrderController

    /**
     * 后台查询订单明细
     * @param page
     * @param pageSize
     * @param number
     * @param beginTime
     * @param endTime
     * @return
     */
    @GetMapping("/page")
    public R<Page<OrdersDto>> empPage(int page, int pageSize, String number,
                                      String beginTime,
                                      String endTime) {
        Page<OrdersDto> empPage = orderService.empPage(page, pageSize, number, beginTime, endTime);
        return R.success(empPage);
    }

订单派送:

    /**
     * 派送订单
     *
     * @param orders
     * @return
     */
    @PutMapping
    public R<String> update(@RequestBody Orders orders) {
        orderService.updateById(orders);
        return R.success("操作成功");
    }

版权声明:程序员胖胖胖虎阿 发表于 2022年9月28日 下午1:56。
转载请注明:瑞吉外卖【后台管理系统篇】 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...