【瑞吉外卖】瑞吉外卖项目笔记

Day1 瑞吉外卖项目概述

软件开发整体介绍

软件开发流程

角色分工

软件环境

软件开发流程

角色分工

软件环境

瑞吉外卖项目介绍

项目介绍

产品原型展示

技术选型

功能架构

角色

项目介绍

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

本项目共分为3期进行开发:

第一期主要实现基本需求,其中移动端应用通过H5实现,用户可以通过手机浏览器访问

第二期主要针对移动端应用进行改进,使用微信小程序实现,用户使用起来更加方便

第三期主要针对系统进行优化升级,提高系统的访问性能

产品原型展示

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

注意:产品原型主要用于展示项目的功能,并不是最终的页面效果

技术选型

功能架构

角色

开发环境搭建

数据库环境搭建

maven项目搭建

数据库环境搭建

 maven项目搭建

1) 配置依赖

<?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.7.10</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.itheima</groupId>
    <artifactId>regie_take_out</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</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>
            <version>8.0.31</version>
            <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>

2)application.yml配置

        spring.application.name是应用的名称,可选

server:
  port: 8080
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:
    #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: ASSIGN_ID

3)编写启动类

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

4)导入前端页面

因为backend和front不在静态资源目录(static和template目录 )下,所以会访问404,可以加以配置

@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport{
    /*
    * 设置静态资源映射
    * */
    @Override
    protected void addResourceHandlers(ResourceHandleRegistry registry){
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
    }
}

备注:

@Configuration
public class MyWebMVCConfig implements WebMvcConfigurer {
    @Value("${file.location}") //         D:/test/ 
    String filelocation;  // 这两个是路径 
    @Value("${file.path}") //         /file/**
    String filepath;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //匹配到resourceHandler,将URL映射至location,也就是本地文件夹
        registry.addResourceHandler(filepath).addResourceLocations("file:///" + filelocation);//这里最后一个/不能不写
    }
}

这段代码的意思就是配置一个拦截器,如果访问的路径是addResourceHandler中的filepath,就把它映射到本地的addResourceLocations的参数的这个路径上,这样就可以让别人访问服务器的本地文件了,比如本地图片、本地音乐视频等等。

访问路径http://localhost:8080/backend/index.html测试配置是否成功:

后台登录功能开发

需求分析

代码开发

功能测试

需求分析

只需要输入用户名和密码就可以登录成功

代码开发

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;

}

备注:①其中@Data是lombok的注解,集成了以下注解:

  • @Getter
  • @Setter
  • @RequiredArgsConstructor
  • @ToString
  • @EqualsAndHashCode

②实现Serializable接口的作用

在程序中为了能直接以 Java 对象的形式进行保存,然后再重新得到该 Java 对象,这就需要序列化能力。

  • 提供一种简单又可扩展的对象保存恢复机制。
  • 对于远程调用,能方便对对象进行编码和解码,就像实现对象直接传输。
  • 可以将对象持久化到介质中,就像实现对象直接存储。
  • 允许对象自定义外部存储的格式。

实现序列化操作用于存储,一般针对于NoSql数据库

③@TableField字段填充策略

描述
DEFAULT默认不处理
INSERT插入填充字段
UPDATE更新填充字段
INSERT_UPDATE插入和更新填充字段

比如在进行插入操作时,会对添加了@TableField(fill = FieldFill.INSERT)的字段进行自动填充;再进行插入和更新操作时,会对添加了@TableField(fill = FieldFill.INSERT_UPDATE)的字段进行自动填充

2)实现后台登录的三层架构框架

第一步:创建包结构

第二步:创建Mapper

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

第三步:创建Service及其实现类

public interface EmployeeService extends IService<Employee> {
}

EmployeeServiceImpl需要实现ServiceImpl,泛型需要指定Mapper以及实体类

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

第四步:创建Controller

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

3)具体实现三层框架代码

第一步:开发返回结果类R

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


/*通用返回结果,服务端响应的数据最终都会封装成此对象
* */
@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<>();
        r.data=object;
        r.code=1;
        return r;
    }

    public static <T> R<T> error(String msg){
        R<T> 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;
    }


}

第二步:在Controller中创建登录方法

1.将页面提交的密码password进行md5加密处理

2.根据页面提交的用户名username查询数据库

3.如果没有查询到则返回登录失败结果

4.密码比对,如果不一致则返回登录失败结果

5.查看员工状态,如果为已禁用状态,则返回员工已禁用结果

6.登录成功,将员工id存入Session并返回登录成功结果

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

    @PostMapping("/login")
    public R<Employee> login(@RequestBody Employee employee, HttpServletRequest request){
        //1.将页面提交的密码password进行md5加密处理
        String password=employee.getPassword();
        password=DigestUtil.md5Hex(password);


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

}

登录成功以后,前端会使用localStorage存储用户信息

备注: ①QueryWrapper的用法

 此外,需要说明的是使用LambdaQueryWrapper,简化lambda使用,避免QueryWrapper的硬编码问题

也可以使用链式查询:

List<BannerItem> bannerItems = new LambdaQueryChainWrapper<>(bannerItemMapper)
                        .eq(BannerItem::getBannerId, id)
                        .list();


BannerItem bannerItem = new LambdaQueryChainWrapper<>(bannerItemMapper)
                        .eq(BannerItem::getId, id)
                        .one();

②session.setAttribute方法解析

B/S架构中,客户端与服务器连接,在服务端就会自动创建一个session对象. session.setAttribute(“username”,username); 是将username保存在session中!session的key值为“username”value值就是username真实的值,或者引用值. 这样以后你可以通过session.getAttribute(“username”)的方法获得这个对象. 比如说,当用户已登录系统后你就在session中存储了一个用户信息对象,此后你可以随时从session中将这个对象取出来进行一些操作,比如进行身 份验证等等.

request.getSession()可以帮你得到HttpSession类型的对象,通常称之为session对象,session对 象的作用域为一次会话,通常浏览器不关闭,保存的值就不会消失,当然也会出现session超时。服务器里面可以设置session的超时时 间,web.xml中有一个session time out的地方,tomcat默认为30分钟

功能测试

分别测试用户名不存在、密码错误、账号已禁用以及登录成功的四个情况即可

后台退出功能开发

需求分析

 点击退出按钮,触发方法logout

此时会调用logoutApi,向/employee/logout发起一个post请求。然后处理返回结果,删除localStorage中存储的userInfo,跳转到login页面

 代码开发

用户点击页面中的退出按钮,发送请求,请求地址为/employee/logout,请求方式为post

我们只需要在Controller中创建对应的处理方法即可,具体的处理逻辑:

1.清理session中的用户id

2.返回结果

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

Day2 员工业务管理开发

完善登录功能

新增员工

员工信息分页查询

启用/禁用员工账号

编辑员工信息

完善登录功能

问题分析

前面我们已经完成了后台系统的员工登录功能开发,但是还存在一个问题:如果用户不登录,直接访问系统首页,照样可以正常访问。

这种设计并不合理,我们希望看到的效果是,只有登陆成功以后才可以访问系统中的页面,如果没有登陆则跳转到登录页面。

那么,具体应该怎么实现呢?

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

过滤器实现步骤:

1.创建自定义过滤器LoginCheckFilter

@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) servletResponse;
        log.info("拦截到请求:{}",request.getRequestURL());
        filterChain.doFilter(request,response);
    }
}

2.在启动类上加上注解@ServletComponentScan

        加上注解@ServletComponentScan,才能扫描到@WebFilter注解

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

代码实现

1.获取本次请求的URI

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

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

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

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

@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.获取本次请求的UTI
        String requestURI = request.getRequestURI();
        //定义不需要处理的请求路径
        String[] urls=new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**"
        };
        //2.判断本次请求是否需要处理
        boolean check = checkURI(urls, requestURI);
        //3.如果不需要处理,则直接放行
        if(check){
            filterChain.doFilter(request,response);
            return;
        }
        //4.判断登录状态,如果已登录,则直接放行
        if(request.getSession().getAttribute("employee")!=null){
            filterChain.doFilter(request,response);
            return;
        }
        //5.如果未登录,则返回未登录结果,通过输出流方式向客户端响应数据
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;
    }

    //路径匹配,检查本次请求是否需要放行
    private boolean checkURI(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是员工的登录账号,必须是唯一的

employee表中的status字段已经设置了默认值1,表示状态正常

代码开发

程序执行的过程:

1.页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端

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

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

    /*
    * 新增员工
    * */
    @PostMapping
    public R<String> save(HttpServletRequest request,@RequestBody Employee employee){
        employee.setPassword(DigestUtil.md5Hex("123456"));
        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("新增员工成功!");
    }

编写全局异常

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

框架搭建:

/*
* 全局异常处理
* */
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
        log.error(ex.getMessage()); //打印错误信息
        return R.error("失败");
    }
}

逻辑完善:

/*
* 全局异常处理
* */
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
    @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.页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端

2.服务端Controller接受页面提交的数据并调用Service查询数据

3.Service调用Mapper操作数据库,查询分页数据

4.Controller将查询到的分页数据响应给页面

5.页面接收到分页数据并通过ElementUI的Table组件展示到页面上

来到index页面,会自动发起员工分页查询

代码开发

第一步:配置分页拦截器

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

第二步:编写Controller

搭建框架:

    @GetMapping("/page")
    public R<Page> page(int page,int pageSize,String name){
        log.info("page={},pageSize={},name={}",page,pageSize,name);
        return null;
    }

逻辑编写:

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

注意,如果StringUtils.isNotEmpty()找不到该方法,注意检查导包是否为:

import org.apache.commons.lang.StringUtils;

功能测试:

补充说明

页面返回的status是0或1,但是显示在页面上的却是已禁用或正常

这是因为使用了<template>模板标签

          <template slot-scope="scope">
            {{ String(scope.row.status) === '0' ? '已禁用' : '正常' }}
          </template>

对于一个数据体[{},{},{},{},...],scope.row相当于一个{},{}里边的数据通过属性名获取

启用/禁用员工账号

需求分析

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

需要注意的是,只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示

代码实现

00

点击禁用/启用按钮,会发送put请求,并携带id和status传递给服务端

启用、禁用员工账号,本质上就是一个更新操作,也就是对status状态字段进行操作

在Controller中创建update方法,此方法是一个通用的修改员工信息的方法

    /*
    * 根据id修改员工信息
    * */
    @PutMapping
    public R<String> update(@RequestBody Employee employee,HttpServletRequest request){
        Long empId=(Long)request.getSession().getAttribute("employee");
        employee.setUpdateUser(empId);
        employee.setCreateTime(LocalDateTime.now());
        employeeService.updateById(employee);

        return R.success("员工信息修改成功!");

    }

问题发现

问题发现:尽管后端已经对status进行了修改,但是前段仍然不会改变。

原因分析:这是因为js对long型数据进行处理时丢失了精度,导致提交的id和数据库中的id不一致。(仔细看,末尾的4位不一致)

解决方案:我们可以在服务端给页面响应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配置类中扩展SpringMVC的消息转换器,在此消息转换器中使用提供的对象转换器进行java对象到json数据的转换

converters.add(0,messageConverter)中,0表示将我们自定义的转换器放在最前面,优先使用

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

功能测试

管理员先禁用张三的账号,退出以后看是否能登陆张三的账号

编辑员工信息

需求分析

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

代码开发

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

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

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

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

5.页面接受服务端响应的json数据,通过vue数据绑定进行员工信息回显

6.点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端

7.服务端接受员工信息,并进行处理,完成后给页面响应

8.页面接收到服务端响应信息后进行相应处理

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

根据id查询用户信息:

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

由于之前已经写过通用的update方法,所以本功能已经实现了

Day3

公共字段自动填充

新增分类

分类信息分页查询

删除分类

修改分类

公共字段自动填充

问题分析

前面我们已经完成了后台系统的员工管理功能开发,在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间和修改人等字段。这些字段属于公共字段,也就是很多表中都有这些字段。

我们考虑用mybatisplus提供的公共字段自动填充功能统一处理

代码实现

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

实现步骤:

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

注意:在自动填充createUser和updateUser时设置的用户id是固定值,需要改造成动态获取当前登录用户的id

尽管我们在用户登录成功后将用户id存入了HttpSession中,但是却不能从HttpSession中获取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);

执行编辑员工功能进行验证,通过观察控制台输出可以发现,一次请求对应的线程id是相同的:

ThreadLocal简介

什么是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封装的工具类

/*
* 基于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

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

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

/*
* 自定义元数据对象处理器
* */
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime",LocalDateTime.now());

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

    }



    @Override
    public void updateFill(MetaObject metaObject) {
        metaObject.setValue("updateTime",LocalDateTime.now());
       
        Long id = BaseContext.getCurrentId();
        metaObject.setValue("updateUser", id);

    }
}

测试通过:

新增分类

第一步:创建实体类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;


    //是否删除
    private Integer isDeleted;

}

第二步:创建Mapper

@Mapper
public interface CategoryMapper extends BaseMapper<Category> {
}

第三步:实现Service及其实现类

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

第四步:实现Controller

@RestController
@RequestMapping("/category")
public class CategoryController {
    @Autowired
    private CategoryService categoryService;

    @PostMapping
    public R<String> save(@RequestBody 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);
    }

删除分类

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

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

要完善分类删除功能,需要先准备基础的类和接口:

1.实体类Dish和Setmeal

/**
 菜品
 */
@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;

}
/**
 * 套餐
 */
@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

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

3.Service接口DishService和SetmealService

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

4.Service实现类DishServiceImpl和SetmealServiceImpl

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

自定义异常,处理分类关联菜品或套餐不能直接删除的情况:

public class CustomException extends RuntimeException{
    public CustomException(String message){
        super(message);
    }
}

在全局异常处理器中加入自定义异常处理器:

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

删除分类逻辑:

@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
    @Autowired
    private DishService dishService;
    @Autowired
    private SetmealService setmealService;
    /*
    * 根据id删除分类,再删除之前需要进行判断
    * */
    @Override
    public void remove(Long id) {
        LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
        //查询当前分类是否关联了菜品,如果已经关联,则抛出一个业务异常
        dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
        int count1 = dishService.count(dishLambdaQueryWrapper);
        if(count1>0){
            //已经关联菜品,抛出一个业务异常
            throw new CustomException("当前分类项关联了菜品,不能删除!");
        }
        //查询当前分类是否关联了套餐,如果已经关联,抛出一个异常
        LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
        setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
        int count2 = setmealService.count(setmealLambdaQueryWrapper);
        if(count2>0){
            //已经关联套餐,抛出一个业务异常
            throw new RuntimeException("当前分类下关联了套餐,不能删除!");
        }
        //正常删除分类
        super.removeById(id);
    }
}

Controller调用:

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

功能测试:

修改分类

在分类管理页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改

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

Day4 菜品管理业务开发

文件上传下载

文件上传介绍

文件上传,也称为upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或者下载的过程。

文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。

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

method="post"        采用post方式提交数据

enctype="multipart/form-data"        采用multipart格式上传文件

type="file"        使用input的file控件上传

举例:

<form method="post" action="/common/upload" enctype="multipart/form-data">
    <input name="myFile" type="file" />
    <input type="submit" value="提交" />
</form>

服务端要接受客户端页面上传的文件,通常都会使用Apache的两个组件:

        commons-fileupload

        commons-io

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

文件下载

文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程

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

以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录

直接在浏览器中打开

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

文件上传代码实现

Controller中形参类型为Multipart,并且文件名和上传过来的文件名name="file"保持一致才能接收

上传的file是一个临时文件,需要转存到指定位置,否则请求完成后临时文件会删除

@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
    @Value("${regie.path}")
    private String basePath;

    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){
        //原始文件名
        String originalFilename=file.getOriginalFilename();
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
        //使用UUID重新生成文件名,方式文件名称重复造成文件覆盖
        String fileName= IdUtil.simpleUUID()+suffix;
        //创建一个目录对象
        File dir=new File(basePath);
        //判断当前目录是否存在
        if(!dir.exists()){
            //目录不存在,需要创建
            dir.mkdirs();
        }
        try {
            file.transferTo(new File(basePath+fileName));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return R.success(fileName);
    }
}

文件下载代码实现

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

    /*
    * 文件下载
    * */
    @GetMapping("/download")
    public void download(String name, HttpServletResponse response){
        //输入流,通过输入流读取文件内容
        try {
            FileInputStream fileInputStream = new FileInputStream(basePath + name);
            //输出流,通过输出流将文件写回浏览器,在浏览器展示图片
            ServletOutputStream outputStream=response.getOutputStream();
            response.setContentType("image/jepg");
            int len=0;
            byte[] bytes = new byte[1024];
            while((len=fileInputStream.read(bytes))!=-1){
                outputStream.write(bytes,0,len);
                outputStream.flush();
            }
            outputStream.close();
            fileInputStream.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

新增菜品

需求分析

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

数据模型

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

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

dish        菜品表

dish_flavor        菜品口味表

代码开发

准备工作

实体类 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<DishFlavor> {
}

Service接口及其实现类

public interface DishFlavorService extends IService<DishFlavor> {
}
@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请求,请求服务端获取菜品分类数据并展示到下拉框中

    @GetMapping("/list")
    public R<List<Category>> list(Category category){
        LambdaQueryWrapper<Category> queryWrapper=new LambdaQueryWrapper<>();
        queryWrapper.eq(category.getType()!=null,Category::getType,category.getType());
        //添加排序条件
        queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
        List<Category> list = categoryService.list(queryWrapper);
        return R.success(list);
    }

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

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

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

因为无法直接用一个实体类接收传递过来的参数,所以要创建一个DTO来封装页面提交过来的数据

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

@Data
public class DishDto extends Dish {

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

    private String categoryName;

    private Integer copies;
}

编写saveWithFlavor逻辑:

public interface DishService extends IService<Dish> {
    //新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish、dish_flavor
    public void saveWithFlavor(DishDto dishDto);
}
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
    @Autowired
    private DishFlavorService dishFlavorService;

    @Override
    @Transactional
    public void saveWithFlavor(DishDto dishDto) {
        //保存菜品的基本信息到菜品表dish
        this.save(dishDto);
        //获取菜品ID
        Long dishId = dishDto.getId();
        //菜品口味
        List<DishFlavor> flavors = dishDto.getFlavors();
        flavors=flavors.stream().map(item->{
            item.setDishId(dishId);
            return item;
        }).collect(Collectors.toList());
        //保存菜品口味数据到菜品口味表dish_flavor
        dishFlavorService.saveBatch(flavors);
    }
}

因为要同时操作两张表,所以需要加@Transactional注解,并且在运行类上加@EnableTransactionManagement

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

补充说明:在SpringBootApplication上使用@ServletComponentScan注解后,Servlet(控制器)、Filter(过滤器)、Listener(监听器)可以直接通过@WebServlet、@WebFilter、@WebListener注解自动注册到Spring容器中,无需其它代码

编写Controller逻辑:

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

菜品信息分页查询

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

    @GetMapping("/page")
    public R<Page<DishDto>> page(int page,int pageSize,String name){
        Page<Dish> pageInfo = new Page<>(page,pageSize);
        Page<DishDto> dishDtoPage=new Page<>();
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(name!=null,Dish::getName,name)
                        .orderByDesc(Dish::getUpdateTime);
        dishService.page(pageInfo,queryWrapper);

        BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");

        List<Dish> records = pageInfo.getRecords();
        List<DishDto> dishDtoList = records.stream().map(item -> {
            DishDto dishDto = new DishDto();
            String categoryName = categoryService.getById(item.getCategoryId()).getName();
            dishDto.setCategoryName(categoryName);
            BeanUtils.copyProperties(item, dishDto);
            return dishDto;
        }).collect(Collectors.toList());
        dishDtoPage.setRecords(dishDtoList);
        
        return R.success(dishDtoPage);
    }

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

修改菜品

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

2.页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显

    @Override
    public DishDto getByIdWithFlavor(Long id) {
        DishDto dishDto = new DishDto();

        Dish dish = this.getById(id);
        BeanUtils.copyProperties(dish,dishDto);

        LambdaQueryWrapper<DishFlavor> dishFlavorLambdaQueryWrapper = new LambdaQueryWrapper<>();
        dishFlavorLambdaQueryWrapper.eq(DishFlavor::getDishId,id);
        List<DishFlavor> flavors = dishFlavorService.list(dishFlavorLambdaQueryWrapper);
        dishDto.setFlavors(flavors);
        
        return dishDto;
    }
    @GetMapping("/{id}")
    public R<DishDto> getDishDto(@PathVariable Long id){
        DishDto dishDto = dishService.getByIdWithFlavor(id);
        return R.success(dishDto);
    }

3.页面发送请求,请求服务端进行图片下载,用于页图片回显

 ===>页面回显成功

4.点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端

    @Override
    @Transactional
    public void updateWithFlavor(DishDto dishDto) {
        //更新dish表基本信息
        this.updateById(dishDto);
        //清理当前菜品对应口味数据
        LambdaQueryWrapper<DishFlavor> dishFlavorLambdaQueryWrapper = new LambdaQueryWrapper<>();
        dishFlavorLambdaQueryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
        dishFlavorService.remove(dishFlavorLambdaQueryWrapper);
        //添加当前提交过来的口味数据
        List<DishFlavor> flavors = dishDto.getFlavors();
        flavors = flavors.stream().map(item -> {
            item.setDishId(dishDto.getId());
            return item;
        }).collect(Collectors.toList());
        dishFlavorService.saveBatch(flavors);
    }
}
    @PutMapping
    public R<String> update(@RequestBody DishDto dishDto){
        dishService.updateWithFlavor(dishDto);
        return R.success("修改菜品成功!");
    }

===>修改菜品成功

Day5 套餐管理业务开发

新增套餐

套餐信息分页查询

删除套餐

新增套餐

需求分析

套餐就是菜品的集合。

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

数据模型

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

setmeal        套餐表

setmeal_dish        套餐菜品关系表

代码开发

准备工作

1.实体类SetmealDish

        冗余字段name和price,可以避免查表dish,减少查表次数

/**
 * 套餐菜品关系
 */
@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;
}

2.DTO SetmealDto

        因为编辑的时候要回显分类名称,所以增加属性categoryName

@Data
public class SetmealDto extends Setmeal {

    private List<SetmealDish> setmealDishes;

    private String categoryName;
}

3.Mapper接口SetmealDishMapper

@Mapper
public interface SetmealDishMapper extends BaseMapper<SetmealDish> {
}

4.业务层接口SetmealDishService

public interface SetmealDishService extends IService<SetmealDish> {
}

5.业务层实现类SetmealDishServiceImpl

@Service
public class SetmealDishServiceImpl extends ServiceImpl<SetmealDishMapper, SetmealDish> implements SetmealDishService {
}

6.控制层SetmealController

@RestController
@RequestMapping("/setmeal")
@Slf4j
public class SetmealDishController {
    @Autowired
    private SetmealDishService setmealDishService;
    @Autowired
    private SetmealService setmealService;
}

梳理交互过程

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

2.页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中

3.页面发送ajax请求,请求服务端根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中

    /*
    * 根据条件查询对应的菜品数据
    * */
    @GetMapping("/list")
    public R<List<Dish>> list(Dish dish){
        //构造查询条件
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId())
                        .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形式提交到服务端

@Service
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService {
    @Autowired
    private SetmealDishService setmealDishService;

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

        //保存套餐和菜品的关联信息
        List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
        setmealDishes=setmealDishes.stream().map(item->{
            item.setSetmealId(setmealDto.getId());
            return item;
        }).collect(Collectors.toList());
        setmealDishService.saveBatch(setmealDishes);
    }
}
    @PostMapping
    public R<String> save(@RequestBody SetmealDto setmealDto){
        setmealService.saveWithDish(setmealDto);
        return R.success("新增套餐成功!");
    }

套餐信息分页查询

需求分析

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

代码开发

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

    @GetMapping("/page")
    public R<Page<SetmealDto>> page(int page,int pageSize,String name){
        Page<Setmeal> pageInfo = new Page<>(page, pageSize);
        Page<SetmealDto> pageDto=new Page<>();

        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(name!=null,Setmeal::getName,name);
        queryWrapper.orderByDesc(Setmeal::getUpdateTime);
        setmealService.page(pageInfo,queryWrapper);

        BeanUtils.copyProperties(pageInfo,pageDto,"records");

        List<Setmeal> records = pageInfo.getRecords();
        List<SetmealDto> setmealDtos = records.stream().map(item -> {
            SetmealDto setmealDto = new SetmealDto();
            BeanUtils.copyProperties(item, setmealDto);

            String categoryName = categoryService.getById(item.getCategoryId()).getName();
            setmealDto.setCategoryName(categoryName);

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

        pageDto.setRecords(setmealDtos);

        return R.success(pageDto);
    }

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

 

删除套餐

需求分析

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

代码实现

    /*
    * 删除套餐,同时需要删除套餐和菜品的关联数据
    * */
    @Override
    @Transactional
    public void removeWithDish(List<Long> ids) {
        //查询套餐状态,确定是否可以删除
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.in(Setmeal::getId,ids)
                .eq(Setmeal::getStatus,1);
        int count = this.count(queryWrapper);
        if(count>0){
            throw new CustomException("套餐正在售卖中,不能删除");
        }

        //如果可以删除,先删除套餐表中的数据
        this.removeByIds(ids);

        //删除关系表中的数据
        LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);

        setmealDishService.remove(lambdaQueryWrapper);
    }
    @DeleteMapping
    public R<String> delete(@RequestParam List<Long> ids){
        setmealService.removeWithDish(ids);
        return R.success("删除套餐成功!");
    }

套餐状态修改代码:

    @PostMapping("status/{status}")
    public R<String> updateStatus(@PathVariable int status,@RequestParam List<Long> ids){
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.in(Setmeal::getId,ids);
        List<Setmeal> setmeals = setmealService.list(queryWrapper);
        setmeals.stream().map(item->{
            if(item.getStatus()==status){
                return R.error("请确保选择的套餐状态一致!");
            }
            return item;
        });

        LambdaUpdateWrapper<Setmeal> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.set(Setmeal::getStatus,status)
                .in(Setmeal::getId,ids);
        setmealService.update(updateWrapper);

        return R.success("套餐状态修改成功!");
    }

Day5 手机验证码登录

短信发送

手机验证码登录

短信发送

短信服务介绍

目前市面上有很多第三方提供的短信服务,这些第三方短信服务会和各个运营商(移动、联通、电信)对接,我们只需要注册成为会员并且按照提供的开发文档进行调用就可以发送短信。需要说明的是,这些短信服务一般都是收费服务。

常用短信服务:

阿里云

华为云

腾讯云

京东

梦网

乐信

阿里云短信服务介绍

阿里云短信服务(Short Message Service)是广大企业客户快速触达手机用户所优选使用的同喜能力。调用API或者用群发助手,即可发送验证码、通知类和营销类短信;国内验证短信妙级触达,到达率可高达99%;国际/港澳台短信覆盖200多个国家和地区,安全稳定,广受出海企业选用

应用场景:

验证码

短信通知

推广短信

请自行按照阿里云官方手册进行短信发送

手机验证码登录

需求分析

为了方便用户登录,移动端通常都会提供手机验证码登录的功能。

手机验证码登录的优点:

方便快捷,无需注册,直接登录

使用短信验证码作为登录凭证,无需记忆密码

安全

登录流程:

输入手机号-->获取验证码-->输入验证码-->点击登录-->登陆成功

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

代码开发

流程梳理

1.在登录页面(front/page/login.html)输入手机号,点击【获取验证码】按钮,页面发送ajax请求,在服务端调用短信服务API给指定的手机号发送验证码短信

2.在登陆页面输入验证码,点击【登录】按钮,发送ajax请求,在服务端处理登录青青

框架搭建

User实体类:

/**
 * 用户信息
 */
@Data
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;


    //姓名
    private String name;


    //手机号
    private String phone;


    //性别 0 女 1 男
    private String sex;


    //身份证号
    private String idNumber;


    //头像
    private String avatar;


    //状态 0:禁用,1:正常
    private Integer status;
}

Mapper接口:

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

Service接口及其实现类:

public interface UserService extends IService<User> {
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

修改LoginCheckFilter:

        //定义不需要处理的请求路径
        String[] urls=new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**",
                "/user/sendMsg", //移动端发送短信
                "/user/login" //移动端登录
        };

 添加如下代码:

        if(request.getSession().getAttribute("user")!=null){
            Long userId = (Long) request.getSession().getAttribute("user");
            BaseContext.setCurrentId(userId);
            filterChain.doFilter(request,response);
            return;
        }

Controller完善

发送验证码:

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
    @Autowired
    private UserService userService;

    @PostMapping("/sendMsg")
    public R<String> sendMsg(@RequestBody User user, HttpSession session){
        //获取手机号
        String phone = user.getPhone();
        //生成随机的4位验证码
        String code = RandomUtil.randomNumbers(4);
        //需要将生成的验证码保存到Session
        session.setAttribute("code",code);

        return R.success(code);
    }
}

登录:

    @PostMapping("/login")
    public R<User> login(@RequestBody Map map,HttpSession session){
        //获取手机号
        String phone =(String) map.get("phone");
        //获取验证码
        String code = (String) map.get("code");
        //从Session中获取保存的验证码
        String codeInSession = (String) session.getAttribute("code");
        //进行验证码比对
        if(codeInSession!=null && codeInSession.equals(code)){
            //如果能够比对成功,说明登陆成功
            LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(User::getPhone,phone);
            User user = userService.getOne(queryWrapper);
            if(user==null) {
                //判断当前手机号对应的用户是否为新用户,如果为新用户就自动完成注册
                user=new User();
                user.setPhone(phone);
                userService.save(user);
            }
            session.setAttribute("user",user.getId());
            return R.success(user);
        }
        return R.error("登录失败,验证码错误!");
    }

登陆成功效果展示:

 

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

导入用户地址簿相关功能代码

菜品展示

购物车

下单

导入用户地址簿相关功能代码

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

功能代码清单:

实体类 AddressBook

/**
 * 地址簿
 */
@Data
public class AddressBook implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;


    //用户id
    private Long userId;


    //收货人
    private String consignee;


    //手机号
    private String phone;


    //性别 0 女 1 男
    private String sex;


    //省级区划编号
    private String provinceCode;


    //省级名称
    private String provinceName;


    //市级区划编号
    private String cityCode;


    //市级名称
    private String cityName;


    //区级区划编号
    private String districtCode;


    //区级名称
    private String districtName;


    //详细地址
    private String detail;


    //标签
    private String label;

    //是否默认 0 否 1是
    private Integer isDefault;

    //创建时间
    @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接口 AddressBookMapper

@Mapper
public interface AddressBookMapper extends BaseMapper<AddressBook> {
}

业务层接口 AddressBookService

public interface AddressBookService extends IService<AddressBook> {
}

业务层实现类 AddressBookServiceImpl

@Service
public class AddressBookServiceImpl extends ServiceImpl<AddressBookMapper, AddressBook> implements AddressBookService {
}

控制层 AddressBookController

@RestController
@RequestMapping("/addressBook")
public class AddressBookController {
    @Autowired
    private AddressBookService addressBookService;

    @PostMapping
    public R<AddressBook> save(@RequestBody AddressBook addressBook){
        addressBook.setUserId(BaseContext.getCurrentId());
        addressBookService.save(addressBook);
        return R.success(addressBook);
    }

    /*
    * 设置默认地址
    * */
    @PutMapping("/default")
    public R<AddressBook> setDefault(@RequestBody AddressBook addressBook){
        LambdaUpdateWrapper<AddressBook> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(AddressBook::getUserId,BaseContext.getCurrentId())
                .set(AddressBook::getIsDefault,0);
        addressBookService.update(updateWrapper);

        addressBook.setIsDefault(1);
        addressBookService.updateById(addressBook);

        return R.success(addressBook);
    }

    /*
    * 查询指定用户的全部地址
    * */
    @GetMapping("/list")
    public R<List<AddressBook>> list(AddressBook addressBook){
        addressBook.setUserId(BaseContext.getCurrentId());

        //条件构造器
        LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(addressBook.getUserId()!=null,AddressBook::getUserId,addressBook.getUserId());
        queryWrapper.orderByDesc(AddressBook::getUpdateTime);

        return R.success(addressBookService.list(queryWrapper));
    }

}

效果展示:

 

菜品展示

需求分析

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

代码开发

梳理交互过程

1.页面(front/index.html)发送ajax请求,获取分类数据(菜品分类和套餐分类)

2.页面发送ajax请求,获取第一个分类下的菜品或者套餐

注意:首页加载完成后,还发送了一次ajax请求用于加载购物车数据,此处可以将这次请求的地址暂时修改一下,从静态json文件获取数据,等后续开发购物车功能时再回来修改

//获取购物车内商品的集合
function cartListApi(data) {
    return $axios({
        // 'url': '/shoppingCart/list',
        "url":"/front/cartData.json",
        'method': 'get',
        params:{...data}
    })
}

效果:

 

改造DishController
    /*
    * 根据条件查询对应的菜品数据
    * */
    @GetMapping("/list")
    public R<List<DishDto>> list(Dish dish){
        //构造查询条件
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId())
                        .eq(Dish::getStatus,1);
        queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
        List<Dish> list = dishService.list(queryWrapper);

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

            Category category = categoryService.getById(item.getCategoryId());
            if(category!=null){
                dishDto.setCategoryName(category.getName());
            }

            LambdaQueryWrapper<DishFlavor> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(DishFlavor::getDishId,item.getId());
            List<DishFlavor> flavors = dishFlavorService.list(wrapper);
            dishDto.setFlavors(flavors);

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

        return R.success(dishDtoList);
    }

效果图:

在SetmealController中提供查询套餐信息
    @GetMapping("/list")
    public R<List<Setmeal>> list(Setmeal setmeal){
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(setmeal.getCategoryId()!=null,Setmeal::getCategoryId,setmeal.getCategoryId())
                .eq(setmeal.getStatus()!=null,Setmeal::getStatus,setmeal.getStatus())
                .orderByDesc(Setmeal::getUpdateTime);

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

购物车

需求分析

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

代码开发

梳理交互过程

1.点击【加入购物车】或者【+】按钮,页面发送ajax请求,请求服务端,将菜品或者套餐添加到购物车

2.点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐

3.点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作

准备工作

实体类ShoppingCart

/**
 * 购物车
 */
@Data
public class ShoppingCart implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //名称
    private String name;

    //用户id
    private Long userId;

    //菜品id
    private Long dishId;

    //套餐id
    private Long setmealId;

    //口味
    private String dishFlavor;

    //数量
    private Integer number;

    //金额
    private BigDecimal amount;

    //图片
    private String image;

    private LocalDateTime createTime;
}

Mapper接口 ShoppingCartMapper

@Mapper
public interface ShoppingCartMapper extends BaseMapper<ShoppingCart> {
}

业务层接口 ShoppingCartService

public interface ShoppingCartService extends IService<ShoppingCart> {
}

业务层实现类 ShoppingCartServiceImpl

@Service
public class ShoppingCartServiceImpl extends ServiceImpl<ShoppingCartMapper,ShoppingCart> implements ShoppingCartService {
}

控制层 ShoppingCartController

@RestController
@RequestMapping("/shoppingCart")
public class ShoppingCartController {
    @Autowired
    private ShoppingCartService shoppingCartService;
    

}

添加购物车
    @PostMapping("/add")
    public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
        //设置用户id,指定当前是哪个用户的购物车数据
        Long currentId = BaseContext.getCurrentId();
        shoppingCart.setUserId(currentId);

        //查询当前菜品或者套餐是否在购物车中
        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ShoppingCart::getUserId,currentId);
        Long dishId = shoppingCart.getDishId();
        if(dishId!=null){
            //添加到购物车的是菜品
            queryWrapper.eq(ShoppingCart::getDishId,dishId);
        }else{
            //添加到购物车的是菜品
            queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
        }
        //查询当前菜品或者套餐是否在购物车中
        ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);
        if(cartServiceOne!=null){
            //如果已存在,就在原来的数量基础上加一
            Integer number = cartServiceOne.getNumber();
            cartServiceOne.setNumber(number+1);
            shoppingCartService.updateById(cartServiceOne);
        }else{
            //如果不存在,则添加到购物车中,数量默认是1
            shoppingCart.setNumber(1);
            shoppingCartService.save(shoppingCart);
            cartServiceOne=shoppingCart;
        }

        return R.success(cartServiceOne);
    }

查看购物车
    @GetMapping("/list")
    public R<List<ShoppingCart>> list(){
        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
        queryWrapper.orderByAsc(ShoppingCart::getCreateTime);

        List<ShoppingCart> list = shoppingCartService.list(queryWrapper);

        return R.success(list);
    }

效果图:

 

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

用户下单

 需求分析

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

数据模型

用户下单业务对应的数据表为orders表和order_detail表:

orders:订单表

order_detail:订单明细表

代码开发

梳理交互过程

1.在购物车中点击【去结算】按钮,页面跳转到订单确认页面

2.在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的默认地址

3.在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的购物车数据

4.在订单确认页面点击【去支付】按钮,发送ajax请求,请求服务端完成下单操作

准备工作

实体类Orders、OrderDetail

/**
 * 订单
 */
@Data
public class Orders implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //订单号
    private String number;

    //订单状态 1待付款,2待派送,3已派送,4已完成,5已取消
    private Integer status;


    //下单用户id
    private Long userId;

    //地址id
    private Long addressBookId;


    //下单时间
    private LocalDateTime orderTime;


    //结账时间
    private LocalDateTime checkoutTime;


    //支付方式 1微信,2支付宝
    private Integer payMethod;


    //实收金额
    private BigDecimal amount;

    //备注
    private String remark;

    //用户名
    private String userName;

    //手机号
    private String phone;

    //地址
    private String address;

    //收货人
    private String consignee;
}
/**
 * 订单明细
 */
@Data
public class OrderDetail implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //名称
    private String name;

    //订单id
    private Long orderId;


    //菜品id
    private Long dishId;


    //套餐id
    private Long setmealId;


    //口味
    private String dishFlavor;


    //数量
    private Integer number;

    //金额
    private BigDecimal amount;

    //图片
    private String image;
}

Mapper接口 OrderMapper、OrderDetailMapper

@Mapper
public interface OrdersMapper extends BaseMapper<Orders> {
}
@Mapper
public interface OrderDetailMapper extends BaseMapper<OrderDetail> {
}

业务层接口 OrderService、OrderDetailService

public interface OrdersService extends IService<Orders> {
}
public interface OrderDetailService extends IService<OrderDetail> {
}

业务层实现类 OrderServiceImpl、OrderDetailServiceImpl

@Service
public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements OrdersService {
}
@Service
public class OrderDetailServiceImpl extends ServiceImpl<OrderDetailMapper,OrderDetail> implements OrderDetailService {
}

控制层 OrderController、OrderDetailController

@RestController
@RequestMapping("/orders")
public class OrdersController {
    @Autowired
    private OrdersService ordersService;
}
@RestController
@RequestMapping("/orderDetail")
public class OrderDetailController {
    @Autowired
    private OrderDetailService orderDetailService;

}

完善Controller

补充得到默认地址:

    @GetMapping("/default")
    public R<AddressBook> getDefault(){
        Long currentId = BaseContext.getCurrentId();

        LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(AddressBook::getUserId,currentId)
                        .eq(AddressBook::getIsDefault,1);
        AddressBook addressBook = addressBookService.getOne(queryWrapper);

        return R.success(addressBook);
    }

插入数据到orders表和orderDetail表:

@Service
public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements OrdersService {
    @Autowired
    private ShoppingCartService shoppingCartService;
    @Autowired
    private UserService userService;
    @Autowired
    private AddressBookService addressBookService;
    @Autowired
    private OrderDetailService orderDetailService;

    @Override
    @Transactional
    public void submit(Orders orders) {
        //获得当前用户id
        Long currentId = BaseContext.getCurrentId();

        //查询当前用户的购物车数据
        LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();
        shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getUserId,currentId);
        List<ShoppingCart> shoppingCarts = shoppingCartService.list(shoppingCartLambdaQueryWrapper);
        if(shoppingCarts==null || shoppingCarts.size()==0){
            throw new CustomException("购物车为空,不能下单!");
        }

        //查询用户数据
        User user = userService.getById(currentId);

        //查询地址数据
        Long addressBookId = orders.getAddressBookId();
        AddressBook addressBook = addressBookService.getById(addressBookId);
        if(addressBook==null){
            throw new CustomException("用户地址信息有误,不能下单!");
        }

        //向订单表插入数据,一条数据
        long orderId = IdWorker.getId();

        AtomicInteger amount=new AtomicInteger(0);
        List<OrderDetail> orderDetails=shoppingCarts.stream().map(item->{
            OrderDetail orderDetail=new OrderDetail();
            orderDetail.setOrderId(orderId);
            orderDetail.setNumber(item.getNumber());
            orderDetail.setDishFlavor(item.getDishFlavor());
            orderDetail.setDishId(item.getDishId());
            orderDetail.setSetmealId(item.getSetmealId());
            orderDetail.setName(item.getName());
            orderDetail.setImage(item.getImage());
            orderDetail.setAmount(item.getAmount());
            amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
            return orderDetail;
        }).collect(Collectors.toList());
        orders.setId(orderId);
        orders.setOrderTime(LocalDateTime.now());
        orders.setCheckoutTime(LocalDateTime.now());
        orders.setStatus(2);
        orders.setAmount(new BigDecimal(amount.get()));
        orders.setUserId(currentId);
        orders.setNumber(String.valueOf(orderId));
        orders.setUserName(user.getName());
        orders.setConsignee(addressBook.getConsignee());
        orders.setPhone(addressBook.getPhone());
        orders.setAddress((addressBook.getProvinceName()==null?"":addressBook.getProvinceName())
                +(addressBook.getCityName()==null?"":addressBook.getCityName())
                +(addressBook.getDistrictName()==null?"":addressBook.getDistrictName())
                +(addressBook.getDetail()==null?"":addressBook.getDetail())
        );
        this.save(orders);

        //向订单明细表插入数据,多条数据
        orderDetailService.saveBatch(orderDetails);
        //清空购物车数据
        shoppingCartService.remove(shoppingCartLambdaQueryWrapper);

    }
}
    @PostMapping("/submit")
    private R<String> submit(@RequestBody Orders orders){
        ordersService.submit(orders);
        return R.success("提交订单成功!");
    }

效果图:

 


《瑞吉外卖》基础部分完结,恭喜大家❀~

  • 4
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值