# 元气外卖
**Repository Path**: xiao-cainiao/reggie
## Basic Information
- **Project Name**: 元气外卖
- **Description**: 本项目是专门为餐厅、饭店定制的一款软件产品,包括系统管理后台和移动端应用两部分。其中系统管理后台主要提供给餐饮内部员工使用,可以对餐厅的菜品、套餐、订单进行管理和维护。移动端应用主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单等。
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 0
- **Created**: 2022-10-05
- **Last Updated**: 2022-10-30
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 概述
## 功能架构图

# 数据库建库建表
## 表说明

# 开发环境
## Maven搭建
直接创建新工程
继承父工程的形式来做这个,这里新建父工程

pom文件
```yml
server:
port: 9001
spring:
application:
name: ccTakeOut
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ruiji?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 333
redis:
host: localhost # 本地IP 或是 虚拟机IP
port: 6379
# password: root
database: 0 # 默认使用 0号db
cache:
redis:
time-to-live: 1800000 # 设置缓存数据的过期时间,30分钟
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
```
## 启动测试
创建测试类并启动

## 导入前端页面

### 导入
在默认页面和前台页面的情况下,直接把这俩拖到resource目录下直接访问是访问不到的,因为被mvc框架拦截了
所以我们要编写一个映射类放行这些资源
#### 创建配置映射类


访问成功

# 后台开发
## 数据库实体类映射
用mybatis plus来实现逆向工程
这里是老版本的逆向工程
```java
org.freemarker
freemarker
2.3.30
com.baomidou
mybatis-plus-boot-starter
3.3.1
com.baomidou
mybatis-plus-generator
3.3.2
```
具体怎么玩看这里
[MP逆向工程教程](https://blog.csdn.net/weixin_48678547/article/details/123379415)

# 账户操作
## 登陆功能
前端页面



数据库

业务逻辑

**这里两个字符串的比较没法用!=来实现**,只能equals再取反来判断
直接上代码,这里没有涉及service层的操作
```java
/**
* @param request 如果登陆成功把对象放入Session中,方便后续拿取
* @param employee 利用@RequestBody注解来解析前端传来的Json,同时用对象来封装
* @return
*/
@PostMapping("/login")
public Result login(HttpServletRequest request, @RequestBody Employee employee) {
String password=employee.getPassword();
String username = employee.getUsername();
log.info("登陆");
//MD5加密
MD5Util md5Util = new MD5Util();
password=MD5Util.getMD5(password);
//通过账户查这个员工对象,这里就不走Service层了
LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper();
lambdaQueryWrapper.eq(Employee::getUsername, username);
Employee empResult=employeeService.getOne(lambdaQueryWrapper);
//判断用户是否存在
if (!empResult.getUsername().equals(username)){
return Result.error("账户不存在");
//密码是否正确
}else if (!empResult.getPassword().equals(password)){
return Result.error("账户密码错误");
//员工账户状态是否正常,1状态正常,0封禁
}else if (empResult.getStatus()!=1){
return Result.error("当前账户正在封禁");
//状态正常允许登陆
}else {
log.info("登陆成功,账户存入session");
//员工id存入session,
request.getSession().setAttribute("employ",empResult.getId());
return Result.success("登陆成功");
}
}
```
具体代码可以参考如下路径
```
com.cc.controller.EmployeeController
```
[关于RequestBody何时使用](https://blog.csdn.net/weixin_44062380/article/details/116103642)
## 退出功能
点击退出

删除session对象
```java
/**
* @param request 删除request作用域中的session对象,就按登陆的request.getSession().setAttribute("employ",empResult.getId());删除employee就行
* @return
*/
@PostMapping("/logout")
public Result login(HttpServletRequest request) {
//尝试删除
try {
request.getSession().removeAttribute("employ");
}catch (Exception e){
//删除失败
return Result.error("登出失败");
}
return Result.success("登出成功");
}
```
## 完善登陆(添加过滤器)
这里的话用户直接url+资源名可以随便访问,所以要加个拦截器,没有登陆时,不给访问,自动跳转到登陆页面

过滤器配置类注解`@WebFilter(filterName="拦截器类名首字母小写",urlPartten=“要拦截的路径,比如/*”)`
判断用户的登陆状态这块之前因为存入session里面有一个名为employee的对象,那么只需要看看这个session还在不在就知道他是否在登陆状态
注意,想存或者想获取的话,就都得用`HttpServletRequest`的对象来进行获取,别的request对象拿不到的
这里提一嘴
调用Spring核心包的字符串匹配类的对象,对路径进行匹配,并且返回比较结果
如果相等就为true
```java
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
```

前端拦截器完成跳转到登陆页面,不在后端做处理

代码太多了,给个路径好啦,直接去Gitee看
request的js代码路径:`resource/backend/js/request.js`
拦截器的路径:`com.cc.filter.LoginCheckFilter`
## 新增员工
新增员工功能,(前端对手机号和身份证号长度做了一个校验)

请求 URL: http://localhost:9001/employee (POST请求)


改造一下Employee实体类,通用id雪花自增算法来新增id

这里用service接口继承的MybatisPlus的功能

注入一下就可以使用了,插入方法

基本上都是自动CRUD,访问路径:`com.cc.controller.EmployeeController`
## 全局异常处理
先看看这种代码的try catch
这种try catch来捕获异常固然好,**但是,代码量一大起来,超级多的try catch就会很乱**

所以我们要加入全局异常处理,在Common包下,和Result同级,这里只是示例,并不完整


当报错信息出现Duplicate entry时,就意味着新增员工异常了
所以,我们对异常类的方法进行一些小改动,让这个异常反馈变得更人性化

这个时候再来客户端试试,就会提供人性化的报错,非常的快乐~

**这回再回到Controller,这时就不需要再来try catch这种形式了,不用管他,因为一旦出现错误就会被我们的AOP捕获。所以,不需要再用try catch来抓了**

异常类位置:`com.cc.common.GloableExceptionHandler`
## 员工信息分页查询
### 接口分析
老生常谈分页查询了
需求

分页请求接口


查询员工及显示接口


逻辑流程

### 分页插件配置类
先弄个MP分页插件配置类
**原因是和3.2.3版本的代码生成器冲突**
[分页插件爆红解决方案](https://blog.csdn.net/weixin_49530535/article/details/119815650)

直接注释掉

加入配置类

### 接口设计
前端注意事项


page对象内部

里面包含了查询构造器的使用
具体的细节在这个包下:com.cc.controller.EmployeeController.page
```java
/**
* 分页展示员工列表接口、查询某个员工
* @param page 查询第几页
* @param pageSize 每页一共几条数据
* @param name 查询名字=name的数据
* @return 返回Page页
*/
@GetMapping("/page")
public Result page(int page, int pageSize,String name){
//分页构造器,Page(第几页, 查几条)
Page pageInfo = new Page(page, pageSize);
//查询构造器
LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper();
//过滤条件.like(什么条件下启用模糊查询,模糊查询字段,被模糊插叙的名称)
lambdaQueryWrapper.like(!StringUtils.isEmpty(name), Employee::getName, name);
//添加排序
lambdaQueryWrapper.orderByDesc(Employee::getCreateTime);
//查询分页、自动更新
employeeService.page(pageInfo, lambdaQueryWrapper);
//返回查询结果
return Result.success(pageInfo);
}
```
## 启用、禁用员工账号
无非就是修改status,0禁用,1启用


这种根据登陆人物来进行判断的玩法,是前端
这个页面的位置`resource/backend/page/member/list.html`

看拿出来的对象是什么样子的,如果是admin,vue的v-if指令就会把编辑按钮显示出来
如果是普通用户,就会把编辑按钮隐藏

### 修复一个小Bug
前端一直不显示编辑按钮,在localStorage里没有发现admin对象

这个值不应该是登陆成功,应该是Employee的对象Json
猜测是登陆的时候往request里存对象没存好

改成对象存入就好了

这回都正常了

### 功能编写
复习一下
==**PutMapping是Resultful风格的请求方式**==

当前状态是1,直接带着目标状态值(状态改禁用)进行更新

Id精度丢失,js独有的bug,直接处理Long处理不了,要Long转String再返回去


利用对象转换器JacksonObjectMapper,将对象转Json
将Long型的Id转换为String类型的数据

在MVC配置类中扩展一个消息转换器

测试功能正常,正常更新员工状态
消息扩展器配置位置:`com.cc.common.JacksonObjectMapper`
对象映射器位置:`com.cc.config.WebMvcConfig`
员工状态更新位置:`com.cc.controller.EmployeeController`
## 编辑员工信息

请求API,这个是先发请求,查到用户,然后填充到页面上
可以看出来,这种请求方式是ResultFul风格的请求方式
在控制器中要用@PathVariable("/{参数名称}")注解来进行接收


完美更新
更新方法位置:`com.cc.controller.EmployeeController.getEmployee`
## 公共字段自动填充
像是一部分公共字段,反复填充起来没有意义,简化填充的操作。
把这个功能拿出来,单独拎出来做自动填充处理


为实体类属性上面加入注解`@TableField(fill = 填充条件)`
看一下源码。fill是填充条件,用枚举来进行处理的

加完注解和条件不算完,还要加入配置类进行处理,对填充的数据做规定
在common包下创建一个自定义类,最关键的是要实现`MetaObjectHandler接口下的insertFill和updateFill`
确认填充时需要的字段。还有要加入@Component注解,将这个类交给框架来管理,否则的话容易找不到,setValue的值会根据注解加入的字段名称来锁定是否需要更新
位置:`com.cc.common.MyMetaObjectHandler`

但是这里有个问题,如果我想去更新管理员字段是非常困难的,因为我这里拿不到Request的作用域对象,所以要想个办法来处理。
这个时候就需要`ThreadLocal`来进行对象的获取,这个线程是贯穿整个运行的,可以通过他来获取
### 使用时
何为ThreadLocal
==**重点来了**==
这个图


我的思路就是在用户登陆的时候,把这个id存进去,等到在填充字段的时候,从ThreadLocal里把这个资源再拿出来。
直接操作不太好,把他封装成一个工具类,这个工具类里方法都是静态的,可以通过类直接调用、并且都是静态方法,来操作保存和读取
我选择在Utils下创建
### 第一次的Bug
具体包在utils里,有Bug,封装的类ThreadLocal获取不到数据,不太清楚为什么,暂时就把这个写死了
```java
// 基于ThreadLocal 封装工具类,用户保存和获取当前登录的用户id
// ThreadLocal以线程为 作用域,保存每个线程中的数据副本
public class BaseContext {
private static ThreadLocal threadLocal = new ThreadLocal<>();
// 设置当前用户id
public static void setCurrentId(Long id){
threadLocal.set(id);
}
public static Long getCurrentId(){
return threadLocal.get();
}
}
```
注意,ThreadLocal不是一个线程,只有同一个线程才能拿到,不是一个线程拿不到的
### 解决方案
更改setId的位置,存储的时候放在过滤器内部,就算是一个线程了,就能拿到。不过我都试过了,确实是一个线程,但是还是拿不到。
换个思路:因为我想拿Request对象里的Id嘛,所以,只要有Request的id就行,不必过于执着一定要用ThreadLocal来存,因此,我这里选择注入一下HttpServletRequest对象来解决这个问题。

# 菜品页面
## 菜品分类

涉及的表有分类表category

业务流程

### 新增菜品分类
请求方式是Post请求

控制器位置:`com.cc.controller.CategoryController (save)`
### 菜品分类展现

还是那几步
1. 创建分页构造器 Page pageInfo = new Page(第几页,每页几条数据);
2. 如果有需要条件过滤的加入条件过滤器LambaQueryWarpper
3. 注入的service对象(已经继承MP的BaseMapper接口)去调用Page对象
service对象.page(分页信息,条件过滤器)
4. 返回结果就可以了
分页查询位置:`com.cc.controller.CategoryController.page`
### 删除菜品分类


普通版本,没有考虑分类有关联的情况

完善一下,==**如果当前菜品分类下有菜品的话,就不许删除**==
所以在删除之前要先做判断才可以删除,不符合条件的,我们要抛出异常进行提示
因为没有返回异常信息的类,我们这里要做一个自定义的专门返回异常信息的类`CustomerException`
这个类的位置也在common包下

因为我们之前创建了一个全局异常处理,也要用上,因为要拦截异常统一处理
还是`com.cc.common.GloableExceptionHandler`
对抛出异常进行处理,就可以对新增的异常提供目标的拦截和异常通知

删除菜品分类的controller接口在:`com.cc.controller.CategoryController (delCategory)`
因为业务特殊,且比较长,就分离出来把业务放在service包下
service接口位置:`com.cc.service.impl.CategoryServiceImpl (removeCategory)`
## 修改套餐信息

非常简单的CRUD,直接调用MP更新一下就行
API位置
```java
com.cc.controller.CategoryController (updateCategory)
```
## 文件上传下载(重点)
### 上传逻辑
第一次接触上传和下载的功能
文件上传逻辑(后端)

参数名有要求的
接收的文件类型一定是 方法名(MultipartFile 前端上传的文件名称)


所以后端的接收名字也得改为file

### 上传逻辑实现
具体的存储路径写在配置文件里了

用@Value注入到业务里就可以了

具体位置在`com.cc.controller.CommonController (upLoadFile)`
### 下载逻辑

==图片回显功能==
用到了输入输出流
位置:`com.cc.controller.CommonController (fileDownload)`
# 菜品管理页面
## 新增菜品
### 需求分析

涉及表为dish和dish_flavor

开发逻辑

### 新增实现
由于是多表的操作,MP直接干肯定不行,所以就把service层抽离出来进行处理
还有,因为涉及两张表,这里还要加入事务进行控制,防止多表操作崩溃
```java
多表操作只能一个一个来,MP没有办法一次性操作多张表
因为涉及到多表的问题,所以还要加入注解来处理事务
@Transactional 开启事务
@EnableTransactionManagement 在启动类加入,支持事务开启
```
Controller位置:`com.cc.controller.DishController (addDish)`
Service位置:`com.cc.service.DishService `
ServiceImpl位置:`com.cc.service.impl.DishServiceImpl (addDishWithFlavor)`
### 新增菜品之获取菜品种类

从前端接收一个type=1的标注,目的是在分类表中,菜品分类是1,套餐分类是2,把二者区分开,获取所有的菜品类型

位置:`com.cc.controller.CategoryController (listCategory)`
### 菜品分页
顺手把菜品分页也做了,不写太多了,位置在:`com.cc.controller.CategoryController (dishPage)`
记录一个知识点,如果说后端没有类和前端要的数据对应,那么自己就可以封装一个类来对前端特殊需要的数据进行封装
## DTO对象
这个类可以是对一些实体类进行扩展,继承于某个父类,再添加一些内容
比如Dish和DishDto
DishDto就继承于Dish类,并在此基础上进行了扩展


## 更新菜品信息
就是个update

逻辑

注意,这里回显数据是要用DishDto,因为前端要显示口味等信息,这里如果用Dish是无法完美显示的,所以要用DishDto
### 回显填充查询

除此之外,这是个多表联查,用MP肯定不行,得自己写
Controller位置:`com.cc.controller.DishController (updateDish)`
Service位置:`com.cc.service.DishService `
ServiceImpl位置:`com.cc.service.impl.DishServiceImpl`
### 更新实现
实际上就是两个表联动更新和删除操作,所以MP直接操作是不可以的,所以要在Service层自己再封装一个删除方法,给Controller层调用删除就行
对于Dish对象可以直接进行更新,因为DishDto是Dish的子类
因此可以调用DishService的update方法传入DishDto对象,来实现Dish的更新
Controller位置:`com.cc.controller.DishController (updateDish)` 确实和上面那个一样,因为请求方式不一样
Service位置:`com.cc.service.DishService `
ServiceImpl位置:`com.cc.service.impl.DishServiceImpl (updateDishWithFlavor)`
### 其他功能
完成一些小功能的开发

#### 停售功能
就是把数据库的status值更新一下,两个路径,一个启售,一个停售

停售请求路径

如果状态不一样了,会从停售变成启售,同时对应的请求路径也不一样


Controller位置:`com.cc.controller.DishController (updateStatusStop)`停止
Controller位置:`com.cc.controller.DishController (updateStatusStart)`启动
#### 删除功能

菜品删除功能
完成逻辑删除,不是真删

位置:
Controller位置:`com.cc.controller.DishController (deleteDish)`停止
# 套餐页面
实际上就是一组菜品的集合
## 新增套餐概述
涉及到的数据库



导入SetmealDto


## 新增套餐之菜品列表


Controller位置:`com.cc.controller.DishController (listCategory)`
## 新增套餐实现
和新增菜品差不多,这里也是多表的操作
Controller位置:`com.cc.controller.SetmealController (saveSetmeal)`
Service位置:`com.cc.service.SetmealService`
ServiceImpl位置:`com.cc.service.impl.SetmealServiceImpl(saveWithDish)`
## 套餐分页
这里的套餐分页和以往不同,设计到了多表内容

套餐分页Controller位置:`com.cc.controller.SetmealController.pageList`
套餐Mapper接口位置:`com.cc.mapper.SetmealMapper`
Mapper文件位置:`resource.mapper.SetmealMapper`

## 更新套餐
添加套餐和更新套餐是几乎完全一致的,字段巴拉巴拉的都一样

但是注意,修改套餐的话,需要先对菜品页面进行填充,这一页都是需要填充满要修改的菜品信息的。
先发请求,一看就是Restful风格请求

获取套餐Controller位置:`com.cc.controller.SetmealController.getSetmal`
## 更新销售状态


和之前一个业务逻辑很像,不想多赘述了,直接放接口位置
Controller位置:`com.cc.controller.SetmealController (startSale/stopSale)`
## 删除套餐
可以单独删,也可以批量删,接口是万金油,都能接,主要看传来的数据是几个


接口

== 多表删除,在Controller直接实现不太现实,所以要在Service把业务写好==
Controller位置:`com.cc.controller.SetmealController (deleteSetmeal)`
Service位置:`com.cc.service.SetmealService`
ServiceImpl位置:`com.cc.service.impl.SetmealServiceImpl(removeWithDish)`

# 前台开发(手机端)
# 账户登陆
## 短信发送


[阿里云短信业务教程](https://blog.csdn.net/qq_55106682/article/details/121920826)
### 代码实现
[官方文档地址](https://help.aliyun.com/document_detail/112148.html)
导入Maven
```java
com.aliyun
aliyun-java-sdk-core
4.5.16
com.aliyun
aliyun-java-sdk-dysmsapi
1.1.0
```

导入短信登陆的工具类,把ACCESSKeyID和Secret更换到位就行

## 验证码发送
数据模型user表,手机验证码专用的表

开发流程

修改拦截器,放行操作


controller位置:`com.cc.controller.UserController (sendMsg)`
发送完还需要验证,验证就是另一个login了
## 用户登陆

controller位置:`com.cc.controller.UserController (login)`
这里登陆还涉及到过滤器放行的功能,不要忘记了,把用户id存入session,过滤器会进行验证
过滤器

controller

# 前台页面
## 导入用户地址簿

地址表

这里直接导入现成的AddressBookController,没有自己写
```java
com.cc.controller.AddressBookController
```
## 菜品展示
逻辑梳理


修改DishController的list方法,来符合前台请求的要求
controller位置:`com.cc.controller.DishController (listCategory)`
套餐内菜品Controller:`com.cc.controller.SetmealController (list)`
## 购物车
把菜品加入购物车


逻辑梳理

注意,这里不需要后端去管总价的计算,就是单价*数量的这个操作,不是后端的内容。前端在展示的时候自己就计算了。
位置:`com.cc.controller.ShoppingCartController (add)`
## 下单

对应的两个表,一个是orders表,另一个是orders_detail表
orders表

orders_detail表

交互流程

业务比较复杂,在Service里写的`com.cc.service.impl.OrdersServiceImpl`
至此基础部分完成,开始对项目性能进行优化
# 小知识点总结
### @RequestBody的使用
只有传来的参数是Json才能用RequestBody接收,如果不是Json的情况(比如那种?key=value&key=value)是不可以用的,会400错误
[关于RequestBody何时使用](https://blog.csdn.net/weixin_44062380/article/details/116103642)
# 缓存优化
基于Redis进行缓存优化

## 环境搭建
### Redis进行配置
加入Pom文件
```xml
org.springframework.boot
spring-boot-starter-data-redis
```
加入Redis配置类
```java
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate