声明:参考资料来源黑马程序员苍穹外卖项目
目录
一、苍穹外卖项目
1、项目介绍
1)定位:专门为餐饮企业定制的一款软件产品;
功能架构:体现项目中的业务功能模块,分为管理端、用户端;
管理端:员工管理、分类管理、菜品管理、套餐管理、订单管理、工作台、数据统计、来单提醒;
用户端:微信登录、商品浏览、购物车、用户下单、微信支付、历史订单、地址管理、用户催单;
产品原型:用于展示项目的业务功能,一般由产品经理进行设计;
技术选型:展示项目中用到的技术框架和中间件等;
2)整体结构
前端工程基于nignx运行;
后端工程基于maven进行项目构建,并进行分模块开发;
项目结构
序号 | 名称 | 说明 |
1 | sky-take-out | maven父工程,统一管理依赖版本,聚合其他子模块 |
2 | sky-common | 子模块,存放公共类,例如:工具类、常量类、异常类等 |
3 | sky-pojo | 子模块,存放实体类、VO、DTO等 |
4 | sky-server | 子模块,后端服务,存放配置文件、Controller、Service、Mapper等 |
3)使用Git进行项目代码的版本控制;
创建Git本地仓库;
创建Git远程仓库;
将本地文件推送到Git远程仓库;
4)通过数据库建表语句创建数据库表结构;
序号 | 表名 | 中文名 |
1 | employee | 员工表 |
2 | category | 分类表 |
3 | dish | 菜品表 |
4 | dish_flavor | 菜品口味表 |
5 | setmeal | 套餐表 |
6 | setmeal_dish | 套餐菜品关系表 |
7 | user | 用户表 |
8 | address_book | 地址表 |
9 | shopping_cart | 购物车表 |
10 | orders | 订单表 |
11 | order_detail | 订单明细表 |
5)nginx反向代理,就是将前端发送动态请求由nginx转发到后端服务器;
nginx反向代理的好处:
提高访问速度;
进行负载均衡(把大量请求按照我们指定的方式均衡的分配给集群中的每台服务器);
保证后端服务安全;
nginx负载均衡策略;
2、完善登陆功能
1)问题:员工表中的密码是明文存储,安全性太低;
解决办法:将密码加密后存储,提高安全性,使用MD5加密方式对明文密码加密;
MD5算法:可以产生出一个128位(16字节)的散列值。
2)将资料中的项目接口导入YApi。
3)Swagger
使用Swagger你只需要按照它的规范去定义接口及接口相关的信息,就可以做到生成接口文档,以及在线接口调试页面;
官网:https://swagger.io/;
Knife4j 是为Java MVC框架集成Swagger生成Api文档的增强解决方案;
使用方式
导入 knife4j 的maven坐标;
在配置类中加入 knife4j 相关配置;
设置静态资源映射,否则接口文档页面无法访问;
接口文档访问路径为 http://ip:port/doc.html;
Yapi 是设计阶段使用的工具,管理和维护接口;
Swagger 在开发阶段使用的框架,帮助后端开发人员做后端的接口测试。
通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下:
3、员工管理、分类管理
1)该部分具体实现功能:
新增员工、员工分页查询、启用禁用员工账号、编辑员工、导入分类模块功能代码;
2)步骤:需求分析和设计、代码开发、功能测试、代码完善;
注意:当前端提交的数据和实体类中对应的属性差别比较大时,建议使用DTO来封装数据;
3)代码开发
在EmployeeController中创建方法,接收前端提交的参数;
在EmployeeService接口中声明方法;
在EmployeeServiceImpl中实现方法;
在EmployeeMapper中声明方法,通过注解或在xml文件中实现;
4)功能测试
通过接口文档测试;
通过前后端联调测试;
注意:由于开发阶段前端和后端是并行开发的,后端完成某个功能后,此时前端对应的功能可能还没有开发完成, 导致无法进行前后端联调测试。所以在开发阶段,后端测试主要以接口文档测试为主。
5)代码完善
新增员工存在的问题
①、录入的用户名已存在,抛出异常后没有处理;
②、新增员工时,创建人id和修改人id设置为了固定值;
解决办法
①、通过全局异常处理器处理;
②、动态获取当前登录员工的id,员工登录成功后会生成JWT令牌并响应给前端,后续请求中,前端会携带JWT令牌,通过JWT令牌可以解析出当前登录员工id,在拦截器中解析出当前登录员工id,并放入线程局部变量中,在Service中获取线程局部变量中的值;
ThreadLocal 并不是一个Thread,而是Thread的局部变量。
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。 ThreadLocal常用方法:
public void set(T value) 设置当前线程的线程局部变量的值;
public T get() 返回当前线程所对应的线程局部变量的值;
public void remove() 移除当前线程的线程局部变量;
员工分页查询存在的问题
操作时间字段展示有问题
解决办法
方式一:在属性上加入注解,对日期进行格式化;
方式二:在 WebMvcConfiguration 中扩展Spring MVC的消息转换器,统一对日期类型进行格式化处理
4、菜品管理
1)公共字段自动填充,公共字段如下:
问题:代码不便于后期维护;
解决办法:
自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法;
自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值;
在 Mapper 的方法上加入 AutoFill 注解。
2)新增菜品
步骤:需求分析和设计、代码开发、功能测试;
业务规则:
菜品名称必须是唯一的;
菜品必须属于某个分类下,不能单独存在;
新增菜品时可以根据情况选择菜品的口味;
每个菜品必须对应一张图片;
接口设计:根据类型查询分类(已完成)、文件上传、新增菜品;
开发文件上传接口:
在yml文件中配置阿里云endpoint、access-key-id、access-key-secret、bucket-name;
创建阿里云Oss工具类;
创建CommonController类,实现文件上传功能;
开发新增菜品接口:
在DishController中创建方法,接收前端提交的参数;
在DishService接口中声明方法;
在DishServiceImpl中实现方法;
在DishMapper中声明方法,在xml中实现批量插入菜品数据和口味数据;
功能测试:
可以通过接口文档进行测试,也可以进行前后端联调测试。
3)菜品分页查询
业务规则: 根据页码展示菜品信息;
每页展示10条数据;
分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询;
根据菜品分页查询接口定义设计对应的DTO来接收前端传的数据;
根据菜品分页查询接口定义设计对应的VO来接收后端返回的数据;
根据接口定义创建DishController的page分页查询方法;
在 DishService 中扩展分页查询方法;
在 DishServiceImpl 中实现分页查询方法;
在 DishMapper 接口中声明 pageQuery 方法;
在 DishMapper.xml 中编写SQL;
功能测试:
可以通过接口文档进行测试,也可以进行前后端联调测试。
4)删除菜品
业务规则: 可以一次删除一个菜品,也可以批量删除菜品;
起售中的菜品不能删除;
被套餐关联的菜品不能删除;
删除菜品后,关联的口味数据也需要删除掉;
代码开发
根据删除菜品的接口定义在DishController中创建方法;
在DishService接口中声明deleteBatch方法;
在DishServiceImpl中实现deleteBatch方法,注意需要判断起售状态和套餐是否关联来决定是否抛出异常,其中异常信息由常量类来提供;
在DishMapper中声明getById方法,并配置SQL;
创建SetmealDishMapper,声明getSetmealIdsByDishIds方法,并在xml文件中编写SQL;
在DishMapper.xml中声明deleteById方法并配置SQL;
功能测试
通过Swagger接口文档进行测试,通过后再前后端联调测试即可。
5)修改菜品
接口设计:
根据id查询菜品;
根据类型查询分类(已实现);
文件上传(已实现);
修改菜品;
代码开发
先查询后修改
在DishController中创建方法,接收前端提交的参数;
在DishService接口中声明方法;
在DishServiceImpl中实现方法;
在DishFlavorMapper中声明方法,通过注解或在xml文件中实现,查询菜品,在DishMapper中声明方法,在xml文件中实现;
功能测试
通过Swagger接口文档进行测试,通过后再前后端联调测试即可。
5、套餐管理
1)新增套餐、根据分类id查询菜品
业务规则:
-
套餐名称唯一
-
套餐必须属于某个分类
-
套餐必须包含菜品
-
名称、分类、价格、图片为必填项
-
添加菜品窗口需要根据分类类型来展示菜品
-
新增的套餐默认为停售状态
接口设计(共涉及到4个接口):
-
根据类型查询分类(已完成)
-
根据分类id查询菜品
-
图片上传(已完成)
-
新增套餐
代码实现
根据分类查询id接口设计
在DishController中创建方法,接收前端提交的参数;
在DishService接口中声明方法;
在DishServiceImpl中实现方法;
在DishMapper中声明方法,在xml文件中实现;
新增套餐接口设计
在SetmealController中创建方法,接收前端提交的参数;
在SetmealService接口中声明方法;
在SetmealServiceImpl中实现方法;
在SetmealMapper中声明方法,在xml文件中实现;
功能测试
通过Swagger接口文档进行测试,通过后再前后端联调测试即可。
2)套餐分页查询
业务规则:
-
根据页码进行分页展示
-
每页展示10条数据
-
可以根据需要,按照套餐名称、分类、售卖状态进行查询
在SetmealController中创建方法,接收前端提交的参数;
在SetmealService接口中声明方法;
在SetmealServiceImpl中实现方法;
在SetmealMapper中声明方法,在xml文件中实现;
功能测试
通过Swagger接口文档进行测试,通过后再前后端联调测试即可。
3)删除套餐
业务规则:
-
可以一次删除一个套餐,也可以批量删除套餐
-
起售中的套餐不能删除
在SetmealController中创建方法,接收前端提交的参数;
在SetmealService接口中声明方法;
在SetmealServiceImpl中实现方法;
在SetmealMapper中声明方法,通过注解实现;
功能测试
通过Swagger接口文档进行测试,通过后再前后端联调测试即可。
4)修改套餐
接口设计(共涉及到5个接口):
-
根据id查询套餐
-
根据类型查询分类(已完成)
-
根据分类id查询菜品(已完成)
-
图片上传(已完成)
-
修改套餐
根据id查询套餐和修改套餐接口设计
在SetmealController中创建方法,接收前端提交的参数;
在SetmealService接口中声明方法;
在SetmealServiceImpl中实现方法;
在SetmealMapper中声明方法,通过注解实现;
功能测试
通过Swagger接口文档进行测试,通过后再前后端联调测试即可。
5)起售停售套餐
业务规则:
-
可以对状态为起售的套餐进行停售操作,可以对状态为停售的套餐进行起售操作
-
起售的套餐可以展示在用户端,停售的套餐不能展示在用户端
-
起售套餐时,如果套餐内包含停售的菜品,则不能起售
在SetmealController中创建方法,接收前端提交的参数;
在SetmealService接口中声明方法;
在SetmealServiceImpl中实现方法;
在SetmealMapper中声明方法,通过注解实现;
功能测试
通过Swagger接口文档进行测试,通过后再前后端联调测试即可。
6、店铺营业状态设置
1)Redis:是一个基于内存的 key-value 结构数据库。
基于内存存储,读写性能高;
适合存储热点数据(热点商品、资讯、新闻);
企业应用广泛;
服务启动命令:redis-server.exe redis.windows.conf;
Redis服务默认端口号为 6379 ,通过快捷键Ctrl + C 即可停止Redis服务;
客户端连接命令:redis-cli.exe;
通过redis-cli.exe命令默认连接的是本地的redis服务,并且使用默认6379端口。也可以通过指定如下参数连接: -h ip地址 -p 端口号 -a 密码(如果需要);
设置Redis服务密码,修改redis.windows.conf;
Redis客户端图形工具:
Redis存储的是key-value结构的数据,其中key是字符串类型,value有5种常用的数据类型:
字符串 string;
哈希 hash;
列表 list;
集合 set;
有序集合 sorted set / zset;
字符串(string):普通字符串,Redis中最简单的数据类型;
哈希(hash):也叫散列,类似于Java中的HashMap结构;
列表(list):按照插入顺序排序,可以有重复元素,类似于Java中的LinkedList ;
集合(set):无序集合,没有重复元素,类似于Java中的HashSet;
有序集合(sorted set / zset):集合中每个元素关联一个分数(score),根据分数升序排序,没有重复元素;
Redis 字符串类型常用命令:
SET key value 设置指定key的值;
GET key 获取指定key的值;
SETEX key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒;
SETNX key value 只有在 key 不存在时设置 key 的值;
Redis hash 是一个string类型的 field 和 value 的映射表,hash特别适合用于存储对象,常用命令:
HSET key field value 将哈希表 key 中的字段 field 的值设为 value;
HGET key field 获取存储在哈希表中指定字段的值;
HDEL key field 删除存储在哈希表中的指定字段;
HKEYS key 获取哈希表中所有字段;
HVALS key 获取哈希表中所有值;
Redis 列表是简单的字符串列表,按照插入顺序排序,常用命令:
LPUSH key value1 [value2] 将一个或多个值插入到列表头部(左边);
LRANGE key start stop 获取列表指定范围内的元素;
RPOP key 移除并获取列表最后一个元素(右边);
LLEN key 获取列表长度;
Redis set 是string类型的无序集合。集合成员是唯一的,集合中不能出现重复的数据,常用命令: SADD key member1 [member2] 向集合添加一个或多个成员
SMEMBERS key 返回集合中的所有成员;
SCARD key 获取集合的成员数;
SINTER key1 [key2] 返回给定所有集合的交集;
SUNION key1 [key2] 返回所有给定集合的并集;
SREM key member1 [member2] 删除集合中一个或多个成员;
Redis有序集合是string类型元素的集合,且不允许有重复成员。每个元素都会关联一个double类型的分数。常用命令:
ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员;
ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员; ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment; ZREM key member [member ...] 移除有序集合中的一个或多个成员;
Redis的通用命令是不分数据类型的,都可以使用的命令:
KEYS pattern 查找所有符合给定模式( pattern)的 key;
EXISTS key 检查给定 key 是否存在;
TYPE key 返回 key 所储存的值的类型;
DEL key 该命令用于在 key 存在是删除 key;
2)在java中操作Redis
Redis 的 Java 客户端很多,常用的几种: Jedis、Lettuce、Spring Data Redis;
Spring Data Redis使用方式:
操作步骤: 导入Spring Data Redis 的maven坐标;
配置Redis数据源;
编写配置类,创建RedisTemplate对象;
通过RedisTemplate对象操作Redis;
//导入坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
//配置数据源
spring:
redis:
host: localhost
port: 6379
password: 123456
//编写配置类
@Configuration
@Slf4j
public class RedisConfiguration{
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
log.info("开始创建redis模板对象");
RedisTemplate redisTemplate = new RedisTemplate();
//设置redis的连接工厂对象
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置redis key的序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
RedisTemplate 针对大量api进行了归类封装,将同一数据类型的操作封装为对应的Operation接口,具体分类如下:
ValueOperations:string数据操作;
SetOperations:set类型数据操作 ;
ZSetOperations:zset类型数据操作;
HashOperations:hash类型的数据操作;
ListOperations:list类型的数据操作;
@SpringBootTest
public class SpringDataRedisTest {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testRedisTemplate(){
System.out.println(redisTemplate);
ValueOperations valueOperations = redisTemplate.opsForValue();
HashOperations hashOperations = redisTemplate.opsForHash();
ListOperations listOperations = redisTemplate.opsForList();
SetOperations setOperations = redisTemplate.opsForSet();
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
}
/**
* 操作字符串类型的数据
*/
@Test
public void testString(){
//set get setex setnx
redisTemplate.opsForValue().set("city","北京");
String city = (String) redisTemplate.opsForValue().get("city");
System.out.println(city);
redisTemplate.opsForValue().set("code","1234",3, TimeUnit.MINUTES);
redisTemplate.opsForValue().setIfAbsent("lock","1");
redisTemplate.opsForValue().setIfAbsent("lock","2");
}
/**
* 操作哈希类型数据
*/
@Test
public void testHash(){
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.put("100","name","tom");
hashOperations.put("100","age","20");
String name = (String) hashOperations.get("100", "name");
System.out.println(name);
Set keys = hashOperations.keys("100");
System.out.println(keys);
List values = hashOperations.values("100");
System.out.println(values);
hashOperations.delete("100","age");
}
/**
* 操作列表类型数据
*/
@Test
public void testList(){
//lpush lrange rpop llen
ListOperations listOperations = redisTemplate.opsForList();
listOperations.leftPushAll("mylist","a","b","c");
listOperations.leftPush("mylist","d");
List mylist = listOperations.range("mylist", 0, -1);
System.out.println(mylist);
listOperations.rightPop("mylist");
Long size = listOperations.size("mylist");
System.out.println(size);
}
/**
* 操作集合类型数据
*/
@Test
public void testSet(){
SetOperations setOperations = redisTemplate.opsForSet();
setOperations.add("set1","a","b","c","d");
setOperations.add("set2","a","b","x","y");
Set members = setOperations.members("set1");
System.out.println(members);
Long size = setOperations.size("set1");
System.out.println(size);
Set intersect = setOperations.intersect("set1", "set2");
System.out.println(intersect);
Set union = setOperations.union("set1", "set2");
System.out.println(union);
setOperations.remove("set1","a","b");
}
/**
* 操作有序集合类型的数据
*/
@Test
public void testZset(){
//zadd zrange zincrby zrem
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
zSetOperations.add("zset1","a",10);
zSetOperations.add("zset1","b",12);
zSetOperations.add("zset1","c",9);
Set zset1 = zSetOperations.range("zset1",0,-1);
System.out.println(zset1);
zSetOperations.incrementScore("zset1","c",10);
zSetOperations.remove("zset1","a","b");
}
/**
* 通用命令操作
*/
@Test
public void testCommon(){
//keys exists type del
Set keys = redisTemplate.keys("*");
System.out.println(keys);
Boolean name = redisTemplate.hasKey("name");
Boolean set1 = redisTemplate.hasKey("set1");
for (Object key : keys) {
DataType type = redisTemplate.type(key);
System.out.println(type.name());
}
redisTemplate.delete("mylist");
}
}
3)店铺营业状态设置
接口设计: 设置营业状态、管理端查询营业状态、用户端查询营业状态;
营业状态数据存储方式:基于Redis的字符串来进行存储,约定:1表示营业 0表示打烊;
在shopController中接收前端参数,通过redisTemlate实现;
在用户端和管理端分别设计查询店铺营业状态接口;
修改代码使接口文档中管理端和用户端接口分开。