瑞吉外卖-基础篇

1. 项目概述

1.1 软件开发整体介绍

  • 软件开发流程
  • 角色分工
  • 软件环境

1.2 瑞吉外卖项目介绍

  • 项目介绍
  • 产品原型展示
  • 技术选型
  • 功能架构
  • 角色

1.3 开发环境搭建

  • 数据库环境搭建
  • maven项目环境搭建

2. 后台登录功能开发

2.1 后台系统登录功能

2022/11/1

后台登录功能开发处理逻辑:

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

controller层处理逻辑代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 员工登录
* @author linsuwen
* @date 2022/11/2 21:34
*/
@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);
}

2.2 后台退出功能开发

2022/11/2

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

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

  1. 清理session中的用户id
  2. 返回结果

处理逻辑代码:

1
2
3
4
5
6
7
8
9
10
11
/**
* 员工退出
* @author linsuwen
* @date 2022/11/2 21:35
*/
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
//清空Session中保存的当前登录员工的id
request.getSession().removeAttribute("employee");
return R.success("退出成功!");
}

3. 员工管理业务开发

3.1 完善登录功能

问题分析

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

具体实现

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

代码实现

实现步骤:

  1. 创建自定义过滤器LoginCheckFilter
  2. 在启动类上加入注解@ServletComponentScan //扫描过滤器
  3. 完善过滤器的处理逻辑

具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/**
* 自定义过滤器:检查用户是否已经完成登录
* @author linsuwen
* @date 2022/11/2 21:37
*/
@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.获取本次请求的url
String requestURL = request.getRequestURI();

//定义不需要处理的url,直接放行
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**", //静态资源放行
"/front/**",
"/user/sendMsg", //移动端发送短信
"/user/login" //移动端登录
};

//2.判断本次请求是否需要处理
boolean check = check(urls, requestURL);

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

//4-1.判断后台管理系统登录状态,如果已登录直接放行
if(request.getSession().getAttribute("employee") != null){

//设置empId
Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);

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

//4-2.判断前台用户登录状态,如果已登录则直接放行
if(request.getSession().getAttribute("user") != null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("user"));

Long userId = (Long) request.getSession().getAttribute("user");
BaseContext.setCurrentId(userId);

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

//5.如果为登录则返回未登录结果,通过输出流方式向客户端页面响应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}

/**
* 路径匹配,检查本次请求是否需要放行
* @author linsuwen
* @date 2022/11/2 21:37
*/
public boolean check(String[] urls, String requestURL){
for(String url : urls){
boolean match = PATH_MATCHER.match(url, requestURL);
if(match){
return true;
}
}
return false;
}
}

3.2 新增员工

新增员工程序的执行过程:

  1. 页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
  2. 服务端controller接收页面提交的数据并调用service将数据进行保存
  3. service调用mapper操作数据库,保存数据

具体逻辑处理代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 添加员工
* @author linsuwen
* @date 2022/11/2 21:43
*/
@PostMapping
public R<String> save(HttpServletRequest request, @RequestBody Employee employee){
log.info("新增员工信息:{}",employee.toString());
//设置初始密码为123456,需要进行MD5加密处理
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
/*
* 公共字段交给MyBatisPlus框架自动填充
* @author linsuwen
* @date 2022/11/6 16:17
*/
// 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表中对该字段加入了唯一约束,此时程序会抛出异常。

此时需要我们的程序进行异常捕获,通常有两种处理方法:

  1. 在controller中加入try、catch进行异常捕获
  2. 使用异常处理器进行全局异常捕获
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 全局异常捕获处理
* @author linmuchun
* @date 2022/11/2 22:05
*/
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 异常处理方法
* @author linsuwen
* @date 2022/11/2 22:15
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException exception){
log.error(exception.getMessage());
if(exception.getMessage().contains("Duplicate entry")){
String[] split = exception.getMessage().split(" ");
String msg = split[2]+"用户已存在!";
return R.error(msg);
}
return R.error("未知错误!");
}
}

3.3 员工信息分页查询

2022/11/3

员工信息分页查询整个程序的执行过程:

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

员工信息分页查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* 员工信息分页查询
* @author linsuwen
* @date 2022/11/3 20:12
*/
@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);
}

3.4 启用/禁用员工账号

启用或禁用员工账号程序的执行过程:

  1. 页面发送ajax请求,将参数(id、status)提交到服务端
  2. 服务端controller接收页面提交的数据并调用service更新数据
  3. service调用mapper操作数据库

启用/禁用员工账号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* 根据id修改员工信息
* @author linsuwen
* @date 2022/11/3 21:22
*/
@PutMapping
public R<String> update(HttpServletRequest request, @RequestBody Employee employee){
log.info(employee.toString());

/*
* 公共字段交给MyBatisPlus框架自动填充
* @author linsuwen
* @date 2022/11/6 16:17
*/
// Long empId = (Long)request.getSession().getAttribute("employee");
// employee.setUpdateTime(LocalDateTime.now());
// employee.setUpdateUser(empId);
employeeService.updateById(employee);

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

功能测试:

测试过程中没有报错,但是功能并没有实现,查看数据库中的数据也没有发生变化。

具体原因:

js处理前端Long型员工id,精度丢失(16位之后精度丢失) Long=>String

代码修复:

  1. 提供对象转换器JacksonObjectMapper,基于Jackson进行java数据到json数据的转换
  2. 在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行java对象到json对象的转换

3.5 编辑员工信息

编辑员工信息操作过程和对应的程序执行流程:

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

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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /*
    * 根据id查询员工信息
    * @author linsuwen
    * @date 2022/11/3 22:21
    */
    @GetMapping("/{id}")
    public R<Employee> getById(@PathVariable Long id){
    Employee employee = employeeService.getById(id);
    if(employee != null){
    return R.success(employee);
    }
    return R.error("没有查询到对应员工信息!");
    }
  5. 页面接收服务端响应的json数据,通过vue的数据绑定进行员工信息回显

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

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

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

4. 分类管理业务开发

4.1 公共字段自动填充

2022/11/6

问题分析

在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间和修改人等字段。这些字段属于公共字段,也就是很多表中都有这些字段

对于这些公共字段可以使用MybatisPlus框架提供的公共字段自动填充功能在某个地方统一处理来简化开发。

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

实现步骤:

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    /*
    * 自定义元数据对象处理器(自动填充配置)
    * @author linsuwen
    * @date 2022/11/6 16:29
    */
    @Slf4j
    @Component
    public class MyMetaObjectHandler implements MetaObjectHandler {
    /*
    * 插入操作自动填充
    * @author linsuwen
    * @date 2022/11/6 16:18
    */
    @Override
    public void insertFill(MetaObject metaObject) {
    log.info("insert公共字段自动填充...");
    log.info(metaObject.toString());
    metaObject.setValue("createTime", LocalDateTime.now());
    metaObject.setValue("updateTime",LocalDateTime.now());
    metaObject.setValue("createUser",BaseContext.getCurrentId());
    metaObject.setValue("updateUser",BaseContext.getCurrentId());
    }

    /*
    * 更新操作自动填充
    * @author linsuwen
    * @date 2022/11/6 16:18
    */
    @Override
    public void updateFill(MetaObject metaObject) {
    log.info("update公共字段自动填充...");
    log.info(metaObject.toString());
    metaObject.setValue("updateTime",LocalDateTime.now());
    metaObject.setValue("updateUser",BaseContext.getCurrentId());
    }
    }

4.2 新增分类

新增分类:

1
2
3
4
5
6
7
8
9
10
11
/*
* 新增分类
* @author linsuwen
* @date 2022/11/6 21:12
*/
@PostMapping
public R<String> save(@RequestBody Category category){
log.info("新增分类:{}",category);
categoryService.save(category);
return R.success("新增分类成功!");
}

4.3 分类信息分页查询

分类信息分页查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* 分页查询
* @author linsuwen
* @date 2022/11/6 21:46
*/
@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);
}

4.4 删除分类

需求分析:

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

删除分类:

1
2
3
4
5
6
7
8
9
10
11
/*
* 根据id删除分类
* @author linsuwen
* @date 2022/11/6 22:17
*/
@DeleteMapping
public R<String> delete(Long ids){
log.info("删除的分类id为:{}",ids);
categoryService.remove(ids); //底层逻辑封装到了service层
return R.success("分类信息删除成功!");
}

4.5 修改分类

修改分类:

1
2
3
4
5
6
7
8
9
10
11
/*
* 根据id修改分类信息
* @author linsuwen
* @date 2022/11/7 9:54
*/
@PutMapping
public R<String> update(@RequestBody Category category){
log.info("修改分类信息:{}",category);
categoryService.updateById(category);
return R.success("修改分类信息成功!");
}

5. 菜品管理业务开发

5.1 文件上传下载

2022/12/5

  • 文件上传代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    /*
    * 文件上传
    * @author linsuwen
    * @date 2022/12/5 18:14
    */
    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){ //注意参数名有要求
    //file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
    log.info(file.toString());

    //原始文件名
    String originalFilename = file.getOriginalFilename();
    String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));

    //使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
    String fileName = UUID.randomUUID().toString()+suffix; //动态拼接文件名后缀

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

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

    return R.success(fileName);
    }
  • 文件下载代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    /*
    * 文件下载
    * @author linsuwen
    * @date 2022/12/5 18:56
    */
    @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();
    }

    fileInputStream.close();
    outputStream.close();
    } catch (Exception e) {
    throw new RuntimeException(e);
    }
    }

5.2 新增菜品

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

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

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

实现步骤:

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /*
    * 根据条件查询分类数据
    * @author linsuwen
    * @date 2022/12/5 21:18
    */
    @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. 页面发送请求进行图片上传,请求服务端将图片保存到服务器

    5.1 文件上传已经完成该部分功能

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

    5.1 文件下载已经完成该部分功能

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

    编写DishDTO,用于封装页面提交的数据(因为传输的数据和实体类不是一一对应的)

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

    1
    2
    3
    4
    5
    6
    @Data
    public class DishDto extends Dish {
    private List<DishFlavor> flavors = new ArrayList<>();
    private String categoryName;
    private Integer copies;
    }

    服务端接收并处理数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /*
    * 新增菜品
    * @author linsuwen
    * @date 2022/12/6 15:56
    */
    @PostMapping
    public R<String> save(@NotNull @RequestBody DishDto dishDto){
    log.info(dishDto.toString());
    dishService.saveWithFlavor(dishDto); //service层
    return R.success("新增菜品成功!");
    }

5.3 菜品信息分页查询

2022/12/6

菜品分页查询时前端页面和服务端交互过程:

  1. 页面(backend/page/food/list.html)发送ajax请求,将分页查询参数(page,pageSize,name)提交到服务端,获取分页数据。
  2. 页面发送请求,请求服务端进行图片下载,用于页面图片展示。

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

菜品信息分页查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/*
* 菜品分页信息查询
* @author linsuwen
* @date 2022/12/6 19:16
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
//构建分页构造器对象
Page<Dish> pageInfo = new Page<>(page,pageSize);
Page<DishDto> dishDtoPage = new Page<>();

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

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

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

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

//对象拷贝
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");

List<Dish> records = pageInfo.getRecords();

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

BeanUtils.copyProperties(item,dishDto);

Long categoryId = item.getCategoryId(); //分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if(category != null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}

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

dishDtoPage.setRecords(list);

return R.success(dishDtoPage);
}

5.4 修改菜品

修改菜品时前端页面(add.html)和服务端的交互过程:

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

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

实现步骤:

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

    5.2 新增菜品时已经完成该部分功能

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /*
    * 根据id查询菜品信息和对应口味信息(数据回显)
    * @author linsuwen
    * @date 2022/12/6 21:09
    */
    @GetMapping("/{id}")
    public R<DishDto> get(@PathVariable Long id){
    DishDto dishDto = dishService.getByIdWithFlavor(id); //主要逻辑封装在了service层
    return R.success(dishDto);
    }
  3. 页面发送请求,请求服务端进行图片下载,用于页面回显

    5.1 文件上传下载时已经完成该部分功能

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /*
    * 修改菜品
    * @author linsuwen
    * @date 2022/12/6 21:51
    */
    @PutMapping
    public R<String> update(@NotNull @RequestBody DishDto dishDto){
    log.info(dishDto.toString());
    dishService.updateWithFlavor(dishDto); //主要逻辑封装在了service层
    return R.success("菜品信息修改成功!");
    }

6. 套餐管理业务开发

6.1 新增套餐

2022/12/7

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

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

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

实现步骤:

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

    前端页面发送的ajax请求:http://localhost:8081/category/list?type=2

    5.2 新增菜品时已经完成该部分功能

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

    前端页面发送的ajax请求:http://localhost:8081/category/list?type=1

    5.2 新增菜品时已经完成该部分功能

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

    前端页面发送的ajax请求:http://localhost:8081/dish/list?categoryId=1397844263642378242

    服务端处理请求:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /*
    * 根据条件查询对应菜品数据
    * @author linsuwen
    * @date 2022/12/7 16:49
    */
    @GetMapping("/list")
    public R<List<Dish>> list(Dish dish){
    //构造查询条件
    LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());

    //添加排序条件
    queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

    List<Dish> list = dishService.list(queryWrapper);

    return R.success(list);
    }
  4. 页面发送请求进行图片上传,请求服务端将图片保存到服务器

    5.1 文件上传下载时已经完成该部分功能

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

    5.1 文件上传下载时已经完成该部分功能

  6. 点击保存按钮,发送ajax请求,将套餐相关数据以json显示提交到服务端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /*
    * 新增套餐
    * @author linsuwen
    * @date 2022/12/7 18:49
    */
    @PostMapping
    public R<String> save(@RequestBody SetmealDto setmealDto){
    log.info("套餐信息:{}",setmealDto);
    setmealService.saveWithDish(setmealDto); //底层逻辑代码封装在了Service层
    return R.success("新增套餐成功!");
    }

6.2 套餐信息分页查询

套餐分页查询时前端页面和服务端的交互过程:

  1. 页面发送ajax请求,将分页查询参数(page,pageSize,name)提交到服务端,获取分页数据
  2. 页面发送请求,请求服务端进行图片下载,用于页面图片展示

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

套餐信息分页查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*
* 套餐分页查询
* @author linsuwen
* @date 2022/12/7 19:37
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
//分页构造对象
Page<Setmeal> pageInfo = new Page<>(page,pageSize);
Page<SetmealDto> setmealDtoPage = new Page<>();

//添加查询条件,根据name进行like模糊查询
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(name!=null,Setmeal::getName,name);

//添加排序条件,根据更新时间降序排列
queryWrapper.orderByDesc(Setmeal::getUpdateTime);

setmealService.page(pageInfo,queryWrapper);

//对象拷贝
BeanUtils.copyProperties(pageInfo,setmealDtoPage,"records");

List<Setmeal> records = pageInfo.getRecords();

List<SetmealDto> list = records.stream().map((item) -> {
SetmealDto setmealDto = new SetmealDto();

BeanUtils.copyProperties(item, setmealDto);

Long categoryId = item.getCategoryId(); //分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if (category != null) {
String categoryName = category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;
}).collect(Collectors.toList());

setmealDtoPage.setRecords(list);

return R.success(setmealDtoPage);
}

6.3 删除套餐

删除套餐时前端页面和服务端的交互过程:

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

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

观察删除单个套餐和批量删除套餐的请求信息可以发现,两种请求的地址和请求方式都是相同的,不同的则是传递id的个数,所以在服务端可以提供一个方法来统一处理。

删除套餐:

删除套餐时要同时操作数据库中两张表的delete操作,底层逻辑代码封装在了service层

1
2
3
4
5
6
7
8
9
10
11
/*
* 删除套餐
* @author linsuwen
* @date 2022/12/7 20:54
*/
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids){
log.info("ids:{}",ids);
setmealService.removeWithDish(ids);
return R.success("套餐删除成功!");
}

7. 手机验证码登录

7.1 短信发送

2022/12/8

7.2 手机验证码登录

登录时前端页面和服务端的交互过程:

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

开发手机验证码登录功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。

8. 菜品展示购物车下单

8.1 导入用户地址簿

2022/12/8

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

8.2 菜品展示

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

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

    QQ截图20221208210120

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    //为了前端页面可以显示菜品规格和口味信息,对改方法进行改进
    @GetMapping("/list")
    public R<List<DishDto>> list(Dish dish){
    //构造查询条件
    LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());
    //添加条件,查询状态为1(起售状态)的菜品
    queryWrapper.eq(Dish::getStatus,1);

    //添加排序条件
    queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

    List<Dish> list = dishService.list(queryWrapper);

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

    BeanUtils.copyProperties(item,dishDto);

    Long categoryId = item.getCategoryId(); //分类id
    //根据id查询分类对象
    Category category = categoryService.getById(categoryId);
    if(category != null){
    String categoryName = category.getName();
    dishDto.setCategoryName(categoryName);
    }

    //当前菜品id
    Long dishId = item.getId();
    LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    lambdaQueryWrapper.eq(DishFlavor::getDishId,dishId);
    List<DishFlavor> dishFlavors = dishFlavorService.list(lambdaQueryWrapper);
    dishDto.setFlavors(dishFlavors);
    return dishDto;
    }).collect(Collectors.toList());

    return R.success(dishDtoList);
    }

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

注意:首页加载完成后,还发送了一次ajax请求用于加载购物车数据,当两次请求全部响应成功后前端页面才会进行渲染并加载显示。

8.3 购物车

2022/12/9

购物车操作时前端页面和服务端的交互过程:

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

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

实现步骤:

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    /*
    * 添加购物车
    * @author linsuwen
    * @date 2022/12/9 21:01
    */
    @PostMapping("/add")
    public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
    log.info("购物车数据:{}",shoppingCart);

    //设置用户id,指定当前是哪个用户的购物车数据
    Long currentId = BaseContext.getCurrentId();
    shoppingCart.setId(currentId);

    Long dishId = shoppingCart.getDishId();
    LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(ShoppingCart::getUserId,currentId);

    if(dishId != null){
    //添加到购物车的是菜品
    queryWrapper.eq(ShoppingCart::getDishId,dishId);
    }else{
    //添加到购物车的是套餐
    queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
    }

    //查询当前菜品或者套餐是否在购物车中
    ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);

    if(cartServiceOne != null){
    //如果已经存在,就在原来数量的基础上加1
    Integer number = cartServiceOne.getNumber();
    cartServiceOne.setNumber(number+1);
    shoppingCartService.updateById(cartServiceOne);
    }else{
    //如果不存在则添加到购物车,数量默认就是1
    shoppingCart.setNumber(1);
    shoppingCart.setCreateTime(LocalDateTime.now());
    shoppingCartService.save(shoppingCart);
    cartServiceOne = shoppingCart;
    }
    return R.success(cartServiceOne);
    }
  2. 点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /*
    * 查看购物车
    * @author linsuwen
    * @date 2022/12/11 21:23
    */
    @GetMapping("/list")
    public R<List<ShoppingCart>> list(){
    log.info("查询购物车...");

    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);
    }
  3. 点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /*
    * 清空购物车
    * @author linsuwen
    * @date 2022/12/11 22:06
    */
    @DeleteMapping("/clean")
    public R<String> clean(){
    LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
    shoppingCartService.remove(queryWrapper);
    return R.success("清空购物车成功!");
    }

8.4 下单

用户下单操作时前端页面和服务端的交互过程:

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

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


瑞吉外卖-基础篇
https://yiqiangshiyia.cn/2022/12/05/瑞吉外卖-基础篇/
作者
一腔诗意啊
发布于
2022年12月5日
许可协议