springboot项目(瑞吉外卖1,后台)

本文详细介绍了瑞奇外卖项目的软件开发流程,包括角色分工、环境搭建、登录与退出功能的实现、员工管理和分类管理等业务模块。此外,还涵盖了文件上传、分页查询、异常处理等技术点,以及系统中如何进行登录检查和公共字段自动填充。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

软件开发整体介绍

软件开发流程

在这里插入图片描述

角色分工

在这里插入图片描述

软件环境

在这里插入图片描述


瑞奇外卖项目介绍

项目介绍

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

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

产品原型展示

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

技术选型

在这里插入图片描述

功能架构

在这里插入图片描述

角色

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


环境搭建

数据库环境搭建

在这里插入图片描述

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

maven环境的搭建

这环境搭建了这么多,还不会怪自己洛

导入静态资源

在config包下创建WebConfig文件,用于配置

package com.ch.reggie.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

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

代码

entity
package com.ch.reggie.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;

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

}

controller service serviceimpl mapper R(给前端的结果集)
@PostMapping("/login")
    public R<Employee> login(HttpRequest httpRequest, @RequestBody Employee employee){

        return  null;
    }
public interface EmployeeService extends IService<Employee> {

}
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper,Employee> implements EmployeeService {

}
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {

}
package com.ch.reggie.common;

import lombok.Data;
import java.util.HashMap;
import java.util.Map;

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

}

登录逻辑

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

package com.ch.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ch.reggie.common.R;
import com.ch.reggie.entity.Employee;
import com.ch.reggie.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@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::getUsername,employee.getUsername());
        Employee emp = employeeService.getOne(queryWrapper);

//        3、如果没有查询到则返回登录失败结果
        if(emp==null){
            return R.error("登录失败");
        }
//        4、密码比对,如果不一致则返回登录失败结果
        if(!emp.getPassword().equals(password)){
            return  R.error("登录失败");
        }
//        5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
        if(emp.getStatus()==0){
            return R.error("账号以禁用");
        }
//        6、登录成功,将员工id存入Session并返回登录成功结果
        request.getSession().setAttribute("employee",emp.getId());

        return  R.success(emp);
    }

}

退出功能的实现

需求分析

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

代码

@PostMapping("/logout")
    public R<String> logout(HttpServletRequest request){
//        1、清理Session中的用户id
        request.getSession().removeAttribute("employee");
//        2、返回结果
        return R.success("退出成功");
    }

完善登录功能

1、过滤器

在这里插入图片描述

  • 步骤
    1、创建自定义过滤器LoginCheckFilter
package com.ch.reggie.filter;

import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 检查用户是否完成登录过滤器
 */
@WebFilter(filterName ="loginCheckFilter" ,urlPatterns = "/*")
@Slf4j
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) filterChain;
        log.info("拦截请求:{}",request.getRequestURI());
        filterChain.doFilter(request,response);
    }
}

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

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

3、完善过滤器的处理逻辑
过滤器具体的处理逻辑如下:
1、获取本次请求的URI

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

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

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

5、如果未登录则返回未登录结果
package com.ch.reggie.filter;

import com.alibaba.fastjson.JSON;
import com.ch.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.util.AntPathMatcher;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 检查用户是否完成登录过滤器
 */
@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();
        log.info("拦截到的请求{}",requestURI);

        //不需要处理的请求
        String[] urls=new String[]{
                "/backend/**",
                "/front/**",
               "/employee/login",
                "/employee/logout"
        };
//        2、判断本次请求是否需要处理
        boolean check = check(urls, requestURI);
//        3、如果不需要处理,则直接放行
        if(check){
            log.info("本次{}请求不需要处理",requestURI);
            filterChain.doFilter(request,response);
            return;
        }
//        4、判断登录状态,如果已登录,则直接放行
        if(request.getSession().getAttribute("employee")!=null){
            log.info("用户已登录,用户id为{}",request.getSession().getAttribute("employee"));
            filterChain.doFilter(request,response);
            return;
        }
//        5、如果未登录则返回未登录结果
        log.info("用户未登录");
        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;
    }
}

新增员工

需求分析

后台系统中可以管理员工信息,通过新增员工来添加后台系统用户。点击 添加员工 按钮跳转到新增页面,如下:
在这里插入图片描述

数据模型

新增员工,其实就是将我们新增页面录入的员工数据插入到employee表。需要注意,employee表中对username字段加入了唯一约束,因为username是员工的登录账号,必须是唯一的

代码开发

![在这里插入图片描述](https://img-blog.csdnimg.cn/002c8b4b5044447ebc4f7513d069881e.png

 @PostMapping
    public R<String> save(HttpServletRequest request,@RequestBody Employee employee){
        log.info("新增员工,员工信息:{}",employee.toString());
        //设置初始密码123456,但需要MD5加密处理
        employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
        //设置系统创建时间
        employee.setCreateTime(LocalDateTime.now());
        //设置更新时间
        employee.setUpdateTime(LocalDateTime.now());
        //设置创建人
        Long empId=(Long) request.getSession().getAttribute("employee");
        employee.setCreateUser(empId);
        //设置更新人
        employee.setUpdateUser(empId);
        employeeService.save(employee);
        return R.success("新增员工成功");
    }

改代码问题分析,如果添加相同的员工会报错,所以需要定义一个全局异常处理的类。

  @ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException e){
        log.error(e.getMessage());
        if(e.getMessage().contains("Duplicate entry")){
            String[] split = e.getMessage().split(" ");
            String msg = split[2] + "用户已存在" ;
            return R.error(msg);
        }
        return R.error("未知错误");
    }
}

通过@ControllerAdvice 配合 @ExceptionHandler 实现全局异常处理


员工信息分页查询

需求分析

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

代码开发

在开发代码之前,需要梳理一下整个程序的执行过程:

1、页面发送ajax请求,将分页查询参数(page.pageSize、name)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面
5、页面接收到分页数据并通过ElementUI的Table组件展示到页面上

1、配置MybatisPlus配置文件,设置分页插件

package com.ch.reggie.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 配置MybatisPlus分页插件
 */
@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();
        //添加过滤条件
        queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
        //添加排序条件
        queryWrapper.orderByDesc(Employee::getUpdateTime);

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

启用/禁用员工账号

需求分析

在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。

需要注意,只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示。
例如:
在这里插入图片描述在这里插入图片描述

代码开发

问题:前端传的数据类型,可能因为最值原因发生改变,而影响结果,所以需要后端制作一个json消息的转换器。
解决:1,2
1、引入已经写好的JacksonObjectMapper对象映射器类

package com.ch.reggie.common;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

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

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

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

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


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

                .addSerializer(BigInteger.class, ToStringSerializer.instance)
                .addSerializer(Long.class, ToStringSerializer.instance)
                .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、在WebMvcConfige配置文件中扩展消息转换器

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

实现代码

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

编辑员工信息

需求分析

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

代码开发

在开发代码之前需要梳理一下操作过程和对应的程序的执行流程:

问题

1、点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]

2、在add.html页面获取url中的参数[员工id]

//获取url地址上面的参数
function requestUrlParam(argname){
  var url = location.href
  var arrStr = url.substring(url.indexOf("?")+1).split("&")
  for(var i =0;i<arrStr.length;i++)
  {
      var loc = arrStr[i].indexOf(argname+"=")
      if(loc!=-1){
          return arrStr[i].replace(argname+"=","").replace("?","")
      }
  }
  return ""

3、发送ajax请求,请求服务端,同时提交员工id参数

4、服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面

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

分类管理业务开发

公共字段自动填充

问题分析

前面我们已经完成了后台系统的员工管理功能开发,在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间和修改人等字段。这些字段属于公共字段,也就是很多表中都有这些字段,如下:
在这里插入图片描述
能不能对于这些公共字段在某个地方统一处理,来简化开发呢?答案就是使用Mybatis Plus提供的公共字段自动填充功能。

代码实现

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接口

package com.ch.reggie.common;

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

import java.time.LocalDateTime;

/**
 * 自定义元数据处理器
 */
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
    /**
     * 插入操作时,自动填充
     *      注意:MetaObjectHandler是无法使用HttpsevletRequet的,无法获取session值。
     *      通过ThreadLocal来实现,他是jdk当中提供了的一个类
     * @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));

    }
}

待优化(获取id问题)

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

有的同学可能想到,用户登录成功后我们将用户id存入了HttpSession中,现在我从HttpSession中获取不就行了?

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

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

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

2、EmployeeContraller的update方法

3、MyMetaObjectHandler的updateFill方法
使用下面代码可以获取当前线程的id

long id = Thread.currentThread().getId() ;

在这里插入图片描述

什么是ThreadLocal?

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

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

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

实现步骤:

1、编写BaseContext工具类,基于ThreadLocal封装的工具类

package com.ch.reggie.common;

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

    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }

    public static Long getCurrentId(){
        return threadLocal.get();
    }
}

2、在LogincheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id(因为更新和插入操作,都必须经过LogincheckFilter过滤器类

if(request.getSession().getAttribute("employee")!=null){
            log.info("用户已登录,用户id为{}",request.getSession().getAttribute("employee"));

            Long employee = (Long) request.getSession().getAttribute("employee");

            //通过写的工具类BaseContext,保存id到ThreadLocal中,方便获取。注意:只有一次请求所经历的方法线程才相同
            BaseContext.setCurrentId(employee);

            filterChain.doFilter(request,response);
            return;
        }

新增分类

需求分析

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

代码开发

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

  • 实体类Category(直接从课程资料中导入即可)
  • Mapper接口CategoryMapper 【@Mapper public interface CategoryMapper extends BaseMapper 】
  • 业务层接口CategoryService 【public interface CategoryService extends IService】
  • 业务层实现类CategoryServicelmpl
    【@Service public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService 】
  • 控制层CategoryController 【@RestController @RequestMapping(“/category”) public class CategoryController 】

在开发代码之前,需要梳理一下整个程序的执行过程:

1、页面(backend/page/category/list.html)发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端

2、服务端Controller接收页面提交的数据并调用Service将数据进行保存

3、Service调用Mapper操作数据库,保存数据

可以看到新增菜品分类和新增套餐分类请求的服务端地址和提交的json数据结构相同,所以服务端只需要提供一个方法统一处理即可
在这里插入图片描述

public R<String> save(@RequestBody Category category){
      log.info("category{}",category);
      categoryService.save(category);
      return R.success("注册成功");
    }

分类信息分页查询

需求分析

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

  @GetMapping("/page")
    public R<Page> page(int page,int pageSize){
        //分页构造器
        Page<Category> pageInfo = new Page<>(page,pageSize);
        //条件过滤器
        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        //添加排序条件,根据sort排序
        queryWrapper.orderByAsc(Category::getSort);

        //分页查询
        categoryService.page(pageInfo,queryWrapper);

        return R.success(pageInfo);
    }

删除分类

需求分析

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

代码开发

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

代码完善

前面我们已经实现了根据id删除分类的功能,但是并没有检查删除的分类是否关联了菜品或者套餐,所以我们需要进行功能完善。

要完善分类删除功能,需要先准备基础的类和接口:
1、实体类Dish和Setmeal (从课程资料中复制即可)
2、Mapper接口DishMapper和SetmealMapper
3、Service接口DishService和SetmealService
4、Service实现类DishServicelmpl和SetmealServicelmpl

关键修改
因为要自己判断是否关联了菜品或者套餐,所以mybatisplues提供的方法没用了,需要自己手动写方法
1、在CategoryService添加remove方法

public interface CategoryService extends IService<Category> {
    public void remove(Long id);
}

2、定义异常类CustomException

package com.ch.reggie.common;

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

3、在全局异常处理器GlobalExceptionHandler添加

@ExceptionHandler(CustomException.class)
    public R<String> exceptionHandler(CustomException e){
        log.error(e.getMessage());
        return R.error(e.getMessage());
    }

4、在CategoryServicelmpl实现remove方法

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

修改分类

需求分析

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

代码实现

在打开的子窗口的显示,前端使用回显已经完成

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

菜品管理业务开发

文件上传

文件上传,也称为upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。

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

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

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

文件上传代码实现

文件上传,页面端可以使用ElementuI提供的上传组件。
可以直接使用资料中提供的上传页面,位置:资料/文件上传下载页面/upload.html

package com.ch.reggie.controller;

import com.ch.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.util.UUID;

/**
 * 文件的上传和下载
 */
@RestController
@Slf4j
@RequestMapping("/common")
public class CommonController {
    /**
     * 文件上传
     * @param file
     * @return
     */
    @Value("${reggie.path}")
    private String basePath;
    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){
        //file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除

//        file.transferTo("D://aa.jpg");  //将零时文件转存
        String originalFilename = file.getOriginalFilename(); //获取原始文件名
        log.info(file.toString());

        //截取原始文件名的后缀
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
        //使用UUID重新生成文件名
        String fileName = UUID.randomUUID().toString()+suffix;

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

        try {
            //basePath:在application中配置的文件地址;
            file.transferTo(new File(basePath+fileName));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return R.success(fileName);
    }
}

在application.yal中

reggie:
  path: D:\\class\\picture\\

文件下载

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

标签展示下载的图片在这里插入图片描述

文件下载代码实现

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

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



    }

新增菜品

需求分析

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

数据模型

新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。所以在新增菜品时,涉及到两个表:
dish 菜品表:
在这里插入图片描述
dish_flavor 菜品口味表:
在这里插入图片描述

代码实现

框架搭建

1、创建实体类DishFlavo
2、Mapper接口DishFlavorMapper

@Mapper
public interface DishFlavorMapper extends BaseMapper<DishFlavor> {
    
}

3、业务层接口DishFlavorService

public interface DishFlavorService extends IService<DishFlavor> {
    
}

4、业务层实现类 DishFlavorServicelmpl

@Service
public class DishFlavorServiceImpl extends ServiceImpl<DishFlavorMapper, DishFlavor> implements DishFlavorService {

}

5、控制层 DishController

/**
 * 菜品管理
 */
@RestController
@RequestMapping("/dish")
public class DishController {
    @Autowired
    private DishService dishService;

    @Autowired
    private DishFlavorService dishFlavorService;

}
梳理交互过程

在开发代码之前,需要梳理一下新增菜品时前端页面和服务端的交互过程:

1、页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中

2、页面发送请求进行图片上传,请求服务端将图片保存到服务器

3、页面发送请求进行图片下载,将上传的图片进行回显

4、点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端

开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。
(1)、将下拉列表显示出(CategoryController)

 @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)、创建DishDto,用于封装页面提交的数据

package com.ch.reggie.dto;

import com.ch.reggie.entity.Dish;
import com.ch.reggie.entity.DishFlavor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;

@Data
public class DishDto extends Dish {

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

    private String categoryName;

    private Integer copies;
}

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

新增菜品同时插入菜品对应的口味数据,需要操作两张表:dish、dishflavor
(3)、在DishService接口中添加方法saveWithFlavor,在DishServiceImpl实现

@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注解

(4)、新增菜品(DishController)

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

菜品信息分页查询

需求分析

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

代码实现

技术难点:在分页展示中,dish里面只有菜品分类id,没有菜品分类名称,需要转化

 @GetMapping("/page")
    public R<Page> page(int page,int pageSize,String name){
        // 构造分页构造器对象
        Page<Dish> pageInfo = new Page<>(page,pageSize);

        //页面中有个菜品分类名称,而dish中只有菜品分类ID,需要使用dishDto中categoryName赋值菜品分类名称。
        Page<DishDto> dishDtoPage = new Page<>();


        // 构造条件构造器
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        //添加过滤条件
        queryWrapper.like(name != null,Dish::getName,name);
        //添加排序条件
        queryWrapper.orderByAsc(Dish::getUpdateTime);

        dishService.page(pageInfo,queryWrapper);

        //对象拷贝
        //将pageInfo类的属性拷贝到dishDtoPage中但不拷贝records属性
        BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");

        List<Dish> records = pageInfo.getRecords();//获取list集合,需要将其处理成 List<DishDto>

        List<DishDto> list= records.stream().map((item) ->{
            DishDto dishDto = new DishDto();
            BeanUtils.copyProperties(item,dishDto);

            Long categoryId = item.getCategoryId(); //拿到每个菜品的分类id
            Category category = categoryService.getById(categoryId);
            if(categoryId != null){
                dishDto.setCategoryName(category.getName());
            }
            return dishDto;
        }).collect(Collectors.toList());

        dishDtoPage.setRecords(list);
        return R.success(dishDtoPage);
    }

菜品信息修改

需求分析

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

代码实现

在开发代码之前,需要梳理一下修改菜品时前端页面( add.html)和服务端的交互过程:

1、页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示

2、页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
DishController处理Get请求
显示代码实现

Controller

@GetMapping("/{id}")
    public R<DishDto> get(@PathVariable Long id){
        DishDto dishDto = dishService.getByidWithFlavor(id);
        return R.success(dishDto);
    }

service实现

@Override
    @Transactional
    public DishDto getByidWithFlavor(Long id) {
        // 查询菜品基本信息
        Dish dish = this.getById(id);

        // 拷贝dish至dishDto中
        DishDto dishDto = new DishDto();
        BeanUtils.copyProperties(dish,dishDto);

        // 通过lambda表达式,添加条件,得到口味信息。
        LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(DishFlavor::getDishId, dish.getId());
        List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);

        dishDto.setFlavors(flavors);
        return dishDto;
    }
更新菜品信息和口味信息

Controller

 @PutMapping
    public R<String> update(@RequestBody DishDto dishDto){
        log.info(dishDto.toString());
        dishService.updateWithFlavor(dishDto);
        return R.success("新增菜品成功");
    }

Service

@Override
    public void updateWithFlavor(DishDto dishDto) {
        // 更新菜品信息
        this.updateById(dishDto);

        // 清理对应的口味信息,delete
        LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
        dishFlavorService.remove(queryWrapper);

        // 添加对应的口味信息,insert
        List<DishFlavor> flavors = dishDto.getFlavors();

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

        dishFlavorService.saveBatch(flavors);
    }

停售/起售菜品,删除菜品

需求分析

在商品买卖过程中,商品停售,起售可以更加方便的让用户知道店家还有什么类型的商品在卖。删除方法也更方便的管理菜品

代码实现

删除菜品代码实现

Controller

@DeleteMapping
    public R<String> delete(String[] ids){
        dishService.deleteWithFlavor(ids);
        return R.success("删除成功");
    }

Service

public void deleteWithFlavor(String[] ids) {
        // 根据菜品id删除菜品信息
        for (String a:ids) {
            this.removeById(a);
            LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper();
            queryWrapper.eq(DishFlavor::getDishId, a);
            dishFlavorService.remove(queryWrapper);
        }
    }
更改停售和启售代码实现

Controller

 @PostMapping("/status/{status}")
    public R<String> sale(@PathVariable int status,String[] ids){
        for (String a:ids) {
            Dish dish = dishService.getById(a);
            dish.setStatus(status);
            dishService.updateById(dish);
        }
        return R.success("修改成功");
    }

套餐管理业务开发

新增套餐

需求分析

套餐就是菜品的集合。

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

在这里插入图片描述

数据模型

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

代码开发-准备工作

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

· 实体类SetmealDish(直接从课程资料中导入即可,Setmeal实体前面+ 课程中已经导入过了)
· DTO SetmealDto(直接从课程资料中导入即可)
· Mapper接口SetmealDishMapper
· 业务层接口SetmealDishService
· 业务层实现类SetmealDishServicelmpl
· 控制层SetmealController

梳理交互过程

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

代码实现

在SetmealServiceImpl实现saveWithDish方法:新增套餐,同时要保持与菜品的关联关系

  @Transactional
    @Override
    public void saveWithDish(SetmealDto setmealDto) {
        // 保存套餐基本信息
        this.save(setmealDto);

        //给SetmealId赋值
        List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
        setmealDishes.stream().map((item)->{
            item.setSetmealId(setmealDto.getId());
            return item;
        }).collect(Collectors.toList());

        // 保存套餐和菜品关联信息
        setmealDishService.saveBatch(setmealDishes);
        
    }

在SetmealController添加save方法

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

套餐分页查询

需求分析

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

梳理交互过程

在开发代码之前,需要梳理一下套餐分页查询时前端页面和服务端的交互过程:

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

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

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

代码实现

@GetMapping("/page")
    public R<Page> page(int page,int pageSize,String name){
        // 分页构造器
        Page<Setmeal> pageInfo = new Page<>(page,pageSize);
        Page<SetmealDto> dtoPage = new Page<>(page,pageSize);
        // 构造条件构造器
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        // 添加查询条件,根据name进行模糊查询
        queryWrapper.like(name != null,Setmeal::getName,name);
        queryWrapper.orderByDesc(Setmeal::getUpdateTime);

        setmealService.page(pageInfo,queryWrapper);

        //将分页查询后的结果,分装到dtoPage中,应为pageInfo中只有套餐id套餐名字。
        BeanUtils.copyProperties(pageInfo,dtoPage,"records");//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){
                setmealDto.setCategoryName(category.getName());
            }
            return setmealDto;
        }).collect(Collectors.toList());
        dtoPage.setRecords(list);

        return R.success(dtoPage);
    }

删除、起售、停售套餐

需求分析

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

删除代码实现

@Override
    @Transactional
    public void deleteWithDish(String[] ids) {
        for (String id : ids) {
            Setmeal setmeal = this.getById(id);
            //如果启售则不能删除
            if(setmeal.getStatus() == 1){
                throw new CustomException(setmeal.getName()+"套餐处于启售状态,删除失败");
            }
            //删除套餐
            this.removeById(id);
            //删除套餐和菜品的关联
            LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(id != null,SetmealDish::getSetmealId,id);
            setmealDishService.remove(queryWrapper);
        }

起售、停售代码实现

@PostMapping("/status/{status}")
    public R<String> onDown(@PathVariable int status,String [] ids){
        for (String id : ids) {
            Setmeal setmeal = setmealService.getById(id);
            setmeal.setStatus(status);
            setmealService.updateById(setmeal);
        }
        return R.success("修改成功");
    }

修改套餐

需求分析

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

梳理交互过程

在开发代码之前,需要梳理一下修改套餐时前端页面( add.html)和服务端的交互过程:
1、页面发送ajax请求,请求服务端获取分类数据,用于套餐分类下拉框中数据展示
2、页面发送ajax请求,请求服务端,根据id查询当前套餐信息,用于套餐信息回显

查询信息代码实现

public SetmealDto getByIdWithDish(Long id) {
        //查询套餐基本信息
        Setmeal setmeal = this.getById(id);
        SetmealDto setmealDto = new SetmealDto();
        BeanUtils.copyProperties(setmeal,setmealDto);

        //查询套餐菜品信息
        LambdaQueryWrapper<SetmealDish> queryWrapper=new LambdaQueryWrapper<>();
        queryWrapper.eq(SetmealDish::getSetmealId,setmeal.getId());
        List<SetmealDish> list = setmealDishService.list(queryWrapper);
        setmealDto.setSetmealDishes(list);
        return setmealDto;
    }

修改套餐信息代码实现

public void updateWithDish(SetmealDto setmealDto) {
        //更新setmeal表基本信息
        this.updateById(setmealDto);

        //更新setmeal_dish表信息delete操作
        LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SetmealDish::getSetmealId, setmealDto.getId());
        setmealDishService.remove(queryWrapper);

        //更新setmeal_dish表信息insert操作
        List<SetmealDish> SetmealDishes = setmealDto.getSetmealDishes();

        SetmealDishes = SetmealDishes.stream().map((item) -> {
            item.setSetmealId(setmealDto.getId());
            return item;
        }).collect(Collectors.toList());

        setmealDishService.saveBatch(SetmealDishes);
    }
### 瑞吉外卖 Spring Boot 示例教程 #### 3.1 Redis集成配置 为了使瑞吉外卖项目能够利用Redis作为缓存机制,在项目的`pom.xml`文件中需引入相应的依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> ``` 此操作确保了应用程序可以访问并操作Redis数据库,从而实现高效的键值存储功能[^1]。 #### 3.2 MyBatis Plus公共字段自动填充 针对分类管理模块中的实体对象,MyBatis Plus框架提供了便捷的方法来设置创建时间和更新时间等通用属性。通过定义一个处理器类,并借助于`ThreadLocal`变量保存当前用户的登录信息,可以在每次新增或修改记录时自动注入这些时间戳数据[^2]。 #### 3.3 分类管理业务逻辑详解 ##### 3.3.1 新增分类 对于新添商品类别这一流程,前端页面提交表单后会触发后台接口调用。服务层负责验证参数合法性以及执行具体的入库动作;而控制层则主要承担接收请求参数并将响应结果返回给客户端的任务。此外,还需考虑如何处理可能发生的各种异常情况,比如重复名称检测失败等问题。 ##### 3.3.2 分页查询 当用户希望浏览已有的菜品分类列表时,系统应支持按条件筛选和分页展示的功能。这涉及到前后端交互的设计——前端发送带有过滤器选项(如关键字匹配)及页码偏移量的AJAX请求至服务器;后者依据传入的信息构建SQL语句进行检索,并最终组装成JSON格式的数据集反馈回去显示在界面上。 ##### 3.3.3 删除与编辑分类 删除指定ID对应的条目前要先确认其是否存在关联子项以免造成孤儿记录现象;至于编辑,则是在保留原有结构的基础上允许更改部分内容而已。无论是哪种情形下都离不开细致严谨的服务端编程工作,包括但不限于事务管理和权限校验等方面的内容。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值