学习 瑞吉外卖项目——总结
本文为个人学习黑马《瑞吉外卖》项目后进行的项目总结,更偏向于对自己编写文本能力的锻炼以及对项目知识点的简短记录。因为个人能力问题,其中可行性分析和测试部分只进行了小标题的陈列,并没有进行编辑。对《瑞吉外卖》项目感兴趣的朋友也可以浏览本文后再去学习,可以对该项目架构有大体感知,同时黑马《瑞吉外卖》非常适合做各位朋友的入门项目,大力推荐本项目。
1.1 项目背景
随着我国城镇和农村居民生活水平达到富裕和小康层次,消费在国民经济活动中的比重逐步加大,居民的餐饮消费逐渐从一日三餐的刚需升级到感受餐饮文化以及社交的重要方式,近年来我国餐饮业销售收入逐年攀升,2019年中国餐饮收入达4.67万亿元,较2018年增加了0.40万亿元,同比增长9.38%,受新冠肺炎疫情影响,2020年中国餐饮收入大幅下滑,随着国内疫情的有效控制,中国餐饮市场也逐渐复苏,2021年中国餐饮收入完成4.69万亿元,较2020年增加了0.74万亿元,同比增长18.64%。“新冠疫情”改变了人们的就餐习惯,对于病毒的心理障碍,更多人选择外卖的方式来就餐,截止2021年12月末中国网上外卖用户规模达54416万人,较2020年同期增加了12533万人,同比增长29.92%。“新冠疫情”改变了人们的就餐习惯,对于病毒的心理障碍,更多人选择外卖的方式来就餐,截止2021年12月末中国网上外卖用户规模达54416万人,较2020年同期增加了12533万人,同比增长29.92%。
1.2 项目介绍
本项目(瑞吉外卖)是专门为餐厅、饭店定制的一款软件产品,包括系统管理后台和移动端应用两部分。其中系统管理后台主要提供给餐饮内部员工使用,可以对餐厅的菜品、套餐、订单进行管理和维护。移动端应用主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单等。

1.3 项目亮点
1、使用Redis进行缓存
当用户数量较多时,系统访问量大,频繁的访问数据库,数据库压力大,系统的性能下降,用户体验感差。因此使用Redis对数据进行缓存,从而减小数据库的压力,在数据更新时删除缓存,从而保证数据库和缓存的一致性,同时有效提高系统的性能和访问速度。
2、使用MySQL主从复制,进行读写分离
读和写数据的所有压力全都由一台数据库承担,压力大,数据库服务器磁盘损坏则数据丢失,单点故障。使用MySQL进行主从复制,主库(master)进行写操作(intsert update delete),从库(salve)进行读操作(select),从而减轻数据库负担,增大系统承受能力,提高系统性能。本项目使用Sharding-JDBC在程序中实现读写分离。
注:MySQL主从复制是一个异步的复制过程,底层是基于Mysql数据库目带的二进制日志功能。就是一台或多台MySQL数据库(slave,即从库)从另一台MySQL数据库(master,即主库)进行日志的复制然后再解析日志并应用到自身,最终实现从库的数据和主库的数据保持一致。MySQL主从复制是MysQL数据库自带功能,无需借助第三方工具。
3、前后端分别部署,使用Nginx进行反向代理
前端页面部署到Nginx服务器中,后端代码部署到后端服务器中,使用Nginx对后端服务器进行反向代理,使用户只需要访问Nginx服务器便可获得后端服务器的服务(便于后期扩展集群,提高系统并发量)。
技术
SpringBoot+SSM 企业级项目
SSM=Spring+Spring MVC+MyBatis

nginx 是服务器,主要部署静态资源。前端请求nginx服务器,再分发到多个tomcat服务器。
Spring Session解决集群的session共享问题。
Swagger:前后端分离使用。
Redis:主要用作缓存。
展示
移动端:展示数据,消费者使用
+
后台管理:维护数据,餐饮企业使用

数据表

一个套餐是多个菜品的集合;
一个菜品有多个口味;
一个顾客可以同时把多个菜品和多个套餐放入购物车中。
@sl4j是lombok提供的
功能开发:
员工管理
1. 后台登录

登录时,以json格式将数据发送至后端
开启驼峰命名
id是雪花算法自动生成的

mp规范:

通用结果类 R
后端返给前端的响应结果


前端使用的是json数据


数据库存储的密码是加密过的
md5加密

错误时,前端展示

2. 后台退出

3. 完善登录




{}是占位符,将会显示逗号后面的内容

传给前端json数据
4. 新增员工

账号设置了唯一的属性,添加相同账号时,数据库抛了异常

全局异常捕获:注解是xxx的类,异常是xxx时,就会被拦截处理
注解是RestController和Controller
@ResponseBody,返回json数据

5.员工信息分页查询



like的第一个条件是if的判断
page查询不需返回,会自动放入pageInfo对象并返回
传给前端的是数字,前端做的解析并展示
6. 启用/禁用员工账号



消息转换器
7. 编辑员工信息


分类管理
1. 公共字段自动填充


功能完善:使用ThreadLocal获取当前用户id
一次请求,涉及的所有方法,属于同一个线程


2. 新增分类




3. 分类信息分页查询

4. 删除分类


dish 菜品
setmeal 套餐
自定义业务异常


全局异常处理器捕获自定义异常

5.修改分类
菜品管理


1.文件上传下载


file首先存储在一个临时文件中,请求完成后会自动删除

参数名必须是file,和前端的name=file保持一致

手动改为jpg,其实是同一个文件

配置文件



使用原始文件名

使用uuid生成随机文件名
加上原始文件的后缀,lastIndexOf()包括了“点”

配置文件,必须存在配置的目录,所以代码需要首先判断是否存在,不存在则创建
File既是文件,又是目录

目录:配置地址,判断是否存在
+
文件:uuid文件名+截取原始后缀
最后,需要给页面传送文件名

文件下载
通过流传给浏览器,所以方法无需返回值,是void

两个流,一个input流,读完的数据放入数组中,然后再将数据放入output流。最后刷新,关闭两个流。
实际上是:input流,读一节数组的长度,放入数组中,流到output流中;再读一节…

2. 新增菜品


菜品表

口味表



菜品的下拉列表,选择菜品分类

新增菜品




3. 菜品信息分页查询


发送了多个请求
此时,菜品分类前端展示有问题,只是id


原始代码:

页面需要dto
拷贝属性,第一个属性:源,第二个属性:目标,第三个属性:不拷贝哪些
records 单独赋值



4. 修改菜品
回显数据

5. 套餐管理
1. 新增套餐



2. 套餐信息分页查询
在开发代码之前,需要梳理一下套餐分页查询时前端页面和服务端的交互过程:
1、页面(backend/page/combo/list.html)发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示
开发套餐信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
请求URL: http://127.0.0.1:8080/setmeal/page
请求方式:GET
参数:page,pageSize,name
controller.SetmealController
@Autowired
private CategoryService categoryService;
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
//分页构造器对象
Page<Setmeal> pageInfo = new Page<>(page,pageSize);
Page<SetmealDto> dtoPage = new Page<>(page,pageSize);
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据name进行like模糊查询
queryWrapper.like(name!= null ,Setmeal::getName,name);
//添加排序条件,根据跟新时间降序排列
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo,queryWrapper);
//拷贝对象
BeanUtils.copyProperties(pageInfo,dtoPage,"records");
List<Setmeal> records = pageInfo.getRecords();
List<SetmealDto> list=records.stream().map((item)->{
SetmealDto setmealDto = new SetmealDto();
//对象拷贝
BeanUtils.copyProperties(item,setmealDto);
//分类id
Long categoryId = item.getCategoryId();
//根据分类id查询分类对象
Category category = categoryService.getById(categoryId);
if(category != null){
String categoryName = category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;
}).collect(Collectors.toList());
dtoPage.setRecords(list);
return R.success(dtoPage);
}
3. 删除套餐
套餐菜品关系表的原因:因为一个菜品可能不属于任何一个套餐啊!所以没有在菜品表中增加一个字段,而是新增了一整张表。
在套餐管理列表页面点击删除按钮,可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。注意,对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。
请求URL:http://127.0.0.1:8080/setmeal
⚝ 请求方式:DELETE
⚝ 参数:ids
参数说明:ids:被删除的id之间用“,”间隔
如:ids:1415580119015145474,1556280893128380418
service.SetmealService
/**
* 删除套餐,同时需要删除套餐和菜品的关联数据
* **/
public void removeWithDish(List<Long> ids);
SetmealServiceImpl
/**
* 删除套餐,同时需要删除套餐和菜品的关联数据
* **/
@Override
@Transactional
public void removeWithDish(List<Long> ids) {
//查询套餐状态,确定是否可用删除(套餐的 status=1,表示套餐正在售卖,不能删除,如果非要删除需要停售套餐)
//select count(*) from setmeal where id in (1,2,3) and status=1;
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Setmeal::getId,ids);
queryWrapper.eq(Setmeal::getStatus,1);
int count = this.count(queryWrapper);
if(count >0){
//如果不能删除,抛出一个业务异常
throw new CustomException("套餐正在售卖中,删除失败");
}
//如果可以删除,先删除套餐表中的数据--setmeal
this.removeByIds(ids);
//删除关系表中的数据--setmeal_dish
//delete from setmeal_dish where setmeal_id in (1,2,3)
LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);
//删除关系表中的数据----setmeal_dish
setmealDishService.remove(lambdaQueryWrapper);
}
controller.SetmealController
//删除与批量删除套餐
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids){
log.info("ids: {}",ids);
setmealService.removeWithDish(ids);
return R.success("套餐数据删除成功");
}
我们进入数据库手动将字段status=0使其停售。以便测试删除功能。
以上就基础版的吉瑞外卖后台管理系统内容!!!
