本篇对苍穹外卖后半部分进行介绍,重点是redis缓存的使用以及微信小程序客户端开发。
目录
一、菜品管理
1.1新增菜品
首先进行需求分析:
注意口味属性也是一个单独的表,要与这个菜品相对应,记录菜品的id,所以涉及到多个表的操作,一个是菜品表一个是口味表,所以要使用@Transactonal开启事务管理
整体流程如下:
在dishcontroller中还是正常的使用dishDTO来接受前端传过来的数据,dishDTO中有flavors数组,前端传过来的口味数组可以用这个数组来接收,但是dish中没有这个属性,然后在service中因为要涉及到两个表的操作,一个是dish表(dish表用于存储菜品)一个是dish_flavor表(这个表用于存放口味,其中有dish_id属性),所以此时要保证事务的原子性,要么都成功,要么都失败所以要在前面添加上@Transactional注解,然后向dish表中插入数据,插入之后再向dish_flavor表中插入数据(创建一个dishflavormapper),dish_flavor表中有dish_id,因为dish_id是自增的,前端没有传过来,那如何得到这个dish_id呢,可以想到前面刚插入一个dish数据,可以通过主键回显来得到这个dish_id,然后在dishMapper.xml文件中是使用usegeneratedkeys然后使用keyproperty=id,赋值给id,然后通过dish。getid即可得到这个值,如果不用主键回显是得不到这个值的。由于flavors是一个数组,所以要给这个数组中每一个元素赋值dish_id,然后再进行批量插入即可,使用foreach。最终完成操作。
界面如下:
整体代码如下:
//Controller:
@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishDTO){
log.info("新增菜品:"+dishDTO);
dishService.saveWithFlavor(dishDTO);
//清理缓存数据:
String key="dish_"+dishDTO.getCategoryId();
redisTemplate.delete(key);
return Result.success();
}
//Service:
@Transactional//需要开启事务管理,可以看到启动类那里已经开启了,添加了这个注解:@EnableTransactionManagement //开启注解方式的事务管理
public void saveWithFlavor(DishDTO dishDTO) {
//向菜品表添加一条数据
//Dish类中没有falvor属性flavor类中有dish_id属性,dishdto类中有List<DishFlavor> flavors属性
Dish dish=new Dish();
BeanUtils.copyProperties(dishDTO,dish);
dish.setStatus(StatusConstant.DISABLE);
dishMapper.insert(dish);
//向dish_flavor表中插入数据需要用到dish_id,但是怎么得到dish_id,这时候要用到主键回显,在dishMapper中使用到了usergeneratedkeys
// 然后返回给这个对象的id属性,然后这个时候使用dish.getId才会有值,如果不使用主键回显是得不到的。
Long dish_id = dish.getId();
//向口味表添加n条数据,因为口味可能有多个,也可能没有
List<DishFlavor> flavors = dishDTO.getFlavors();
if(flavors.size()>0&&flavors!=null){
//这里要进行赋值,dish_id:
flavors.forEach(dish_flavor->{
dish_flavor.setDishId(dish_id);
});
//注意此处flavors是一个数组,想进行插入操作可以批量插入:
dishFlavorMapper.insertBach(flavors);
}
}
Mapper:
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into dish(name, category_id, price, image, status,description, create_time, update_time, create_user, update_user)
values(#{name},#{categoryId},#{price},#{image},#{status},#{description},#{createTime},#{updateTime},#{createUser},#{updateUser})
</insert>
!--注意此处虽然dish_flavor中有dish_id属性,但是前端还没有把这个dish_id传过来,因为主键一般是自增的,
并且还在上传菜品中还没有提交此时还没有值呢,那如何获取dish_id呢,可以想到DishServiceImpl中在dish表插入了一条数据,可以进行主键回显,然后得到这个dish_id这个值,-->
<insert id="insertBach">
insert into dish_flavor(dish_id,name,value)values
<foreach collection="flavors" item="df" separator=",">
(#{df.dishId},#{df.name},#{df.value})
</foreach>
</insert>
1.2菜品的分页查询
需求分析:需要返回以下数据,需要返回分类名称,所以还需要前端传递分类id,根据分类id去查询分类表得到分类名称,需要连表进行查询,最终设计一个VO封装返回数据。
整体代码如下:
需要注意的就是需要联合查询返回分类名称,这一步容易忽略,此处采用左外连接以及动态查询的方式来获取到categoryname然后封装到dishVO中;注意此处采用左外连接查询,查询dish表的所有加category表中的name,利用category的id相同;
另外就是使用分页插件,PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize()) 这一步的作用就是:
设置分页参数 :设置了分页的参数,包括当前请求的页码(
getPage()
)和每页显示的记录数(getPageSize()
)。拦截后续查询 :在调用
startPage
方法后,PageHelper 会拦截紧随其后的第一次 MyBatis 查询操作,并自动为其添加分页的 SQL 语句。自动分页:不需要手动编写分页的 SQL 语句,PageHelper 会自动处理分页逻辑,包括计算总页数、总记录数等。
//Controller
/**
* 分类查询
* @param dishPageQueryDTO
* @return
*/
@ApiOperation("菜品分页查询:")
@GetMapping("/page")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
PageResult pageResult=dishService.page(dishPageQueryDTO);
return Result.success(pageResult);
}
//Service
public PageResult page(DishPageQueryDTO dishPageQueryDTO) {
PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());
Page<DishVO> page=dishMapper.select(dishPageQueryDTO);
long total = page.getTotal();
List result = page.getResult();
return new PageResult(total,result);
}
//DAO:
Page<DishVO> select(DishPageQueryDTO dishPageQueryDTO);
<select id="select" resultType="com.sky.vo.DishVO">
select d.*,c.name as categoryName from dish d left outer join category c on d.category_id=c.id
<where>
<if test="name!=null">
and d.name like concat('%',#{name},'%')
</if>
<if test="status!=null">
and d.status=#{status}
</if>
<if test="categoryId!=null">
and d.category_id=#{categoryId}
</if>
</where>
order by d.create_time desc
</select>
1.3删除菜品
需求分析:需求分析是很重要的,需要考虑的点也很多,业务层需要做很多的逻辑判断,并且还需要连带口味表中的数据也需要删除。
需要判断菜品是否起售,需要根据id去查询菜品的状态,如果起售状态无法删除。然后需要去判断菜品是否被套餐关联,可以根据菜品id去套餐表中查询数量,如果数量大于零说明已经被关联,无法删除。
需要注意的是前端可以传递多个菜品id批量删除,所以需要用一个LIst接收参数,并且使用@RequestParam注解,前端传过来的是String类型的,1,2,3这种类型的,所以正常来说要用String来接受,然后split分隔成数组,但是现在可以用springmvc来自动解析,然后转换成一个集合。
此外,也需要操作多个表,菜品表和口味表都需要进行删除操作,所以需要开启事务。
整体代码如下:
//Controller
@DeleteMapping
@ApiOperation("删除菜品")
public Result delete(@RequestParam List<Long> ids){//注意此处前端传过来的是String类型的,1,2,3这种类型的,所以正常来说要用String来接受,然后split分隔成数组,但是现在可以用springmvc来自动解析,然后转换成一个集合,如果这样的话,需要用一个注解!
dishService.deleteBatch(ids);
//清理缓存:此处将所有缓存数据都要删除,首先要获取所有的key
Set keys = redisTemplate.keys("dish_*");//此处表示获取所有以dish_开头的key
redisTemplate.delete(keys);//支持集合参数
return Result.success();
}
//Service:
@Transactional
public void deleteBatch(List<Long> ids) {
//首先要判断能否删除,起售中的菜品无法被删除:
for(Long id:ids){
Dish dish=dishMapper.getById(id);
if(dish.getStatus()== StatusConstant.ENABLE){
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}
//被套餐关联的菜品不能被删除:
int count=setmealDishMapper.getCount(ids);
if(count>0){
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
dishMapper.delete(ids);
dishFlavorMapper.delete(ids);
}
//DAO:
<delete id="delete">
delete from dish where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
<delete id="delete">
delete from dish_flavor where dish_id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
1.4修改菜品
首先需要进行查询操作回显,然后再进行更新操作。需要注意的是回显的时候会涉及到多个表,例如口味表等 。
但是此处口味表的修改非常复杂,因为修改的时候可能添加可能删除可能修改,所以一个方法就是直接删除掉该菜品联合的口味,然后再重新新增。
@ApiOperation("根据id查询菜品用于修改菜品回显")
@GetMapping("/{id}")
public Result<DishVO> getById(@PathVariable Long id){
log.info("根据id查询菜品:"+id);
DishVO dishvo=dishService.getById(id);
return Result.success(dishvo);
}
@ApiOperation("修改菜品")
@PutMapping
public Result update(@RequestBody DishDTO dishDTO){
log.info("修改菜品"+dishDTO);
dishService.update(dishDTO);
//清理缓存:此处将所有缓存数据都要删除,首先要获取所有的key
Set keys = redisTemplate.keys("dish_*");//此处表示获取所有以dish_开头的key
redisTemplate.delete(keys);//支持集合参数
return Result.success();
}
//根据id查询菜品
public DishVO getById(Long id) {
Dish dish=dishMapper.getById(id);
List<DishFlavor> flavors=dishFlavorMapper.getById(id);
DishVO dishVO=new DishVO();
BeanUtils.copyProperties(dish,dishVO);
dishVO.setFlavors(flavors);
return dishVO;
}
//修改菜品
public void update(DishDTO dishDTO) {
Dish dish=new Dish();
BeanUtils.copyProperties(dishDTO,dish);
dishMapper.update(dish);
//此处口味的修改非常复杂,因为可能会删除可能会新增还可能不修改,所以一种方法是直接都删掉,然后新增:
List<Long> list=new ArrayList();
list.add(dishDTO.getId());
dishFlavorMapper.delete(list);
List<DishFlavor> flavors = dishDTO.getFlavors();
if(flavors.size()>0&&flavors!=null){
//注意此处flavors是一个数组,想进行插入操作可以批量插入:
flavors.forEach(dish_flavor->{
//此处仍然需要设置dish_id,原来的都删除掉了,然后新增之后没有dish_id了,还需要在此处设置一下
dish_flavor.setDishId(dishDTO.getId());
});
dishFlavorMapper.insertBach(flavors);
}
}
1.5设置营业状态
本来可以使用一个设置接口就可以,管理端和用户端共用这个接口,但是在本项目中用户端的发送请求都是/admin为前缀,用户端都是/user为前缀,所以此处写了两个查询接口。
需求分析:
因为定义了两个shopcontroller,会有冲突,所以可以分别在user和admin中的RestController注解中写上对应名称。
二、微信小程序客户端的开发
首先介绍一下HttpClient:
其中有几个核心API:
httpclient是一个接口可以发送http请求,httpclients可以创建一个httpclient,CloseableHttpClient是一个实现类,实现了HttpClient接口。HttpGet请求和HttpPost请求。
测试get和post请求,调用我们的接口:
@SpringBootTest
public class HttpClientTest {
/**
* 通过HttpClient发送get方式请求
*/
@Test
public void testGet() throws IOException {
//创建httpclient对象
CloseableHttpClient httpClient= HttpClients.createDefault();
//创建请求对象
HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");//去访问user端的接口
//发送请求,接收响应结果
CloseableHttpResponse response=httpClient.execute(httpGet);
//获取服务端响应过来的状态码
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("服务端发送过来的状态码:"+statusCode);
//获取响应过来的值:
HttpEntity entity = response.getEntity();
String s = EntityUtils.toString(entity);
System.out.println("服务端响应回来的值:"+s);
//关闭资源
response.close();
httpClient.close();
}
/**
* 通过HttpClient发送post方式请求
*/
@Test
public void testPost() throws IOException {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");//这是一个post请求,请求的是管理端的登录接口,需要传过去json格式的数据
JSONObject jsonObject=new JSONObject();
jsonObject.put("username","admin");
jsonObject.put("password","123456");
StringEntity entity=new StringEntity(jsonObject.toString());
//指定请求的编码方式:
entity.setContentEncoding("utf-8");
//数据格式:
entity.setContentType("application/json");
httpPost.setEntity(entity);
//发送请求
CloseableHttpResponse response = httpClient.execute(httpPost);
//解析结果:
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("响应码为:"+statusCode);
HttpEntity entity1=response.getEntity();
String body = EntityUtils.toString(entity1);
System.out.println("响应数据为"+body);
//关闭资源:
response.close();
httpClient.close();
}
}
微信登录即可完成登录,如果是新用户,自动注册将用户保存到数据库中,要实现微信登陆,就需要获取授权码,小程序先wx.login获取到授权码(code) 然后发送请求到服务端,服务端接收code后发送http请求到微信接口服务,然后微信接口返回session_key和openid。然后服务端接收之后,需要给用户返回令牌,令牌中包含用户的唯一标识。
同样需要配置客户端的jwt登录校验:
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
@Autowired
private JwtTokenUserInterceptor jwtTokenUserInterceptor;
/**
* 注册自定义拦截器
*
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/employee/login");
registry.addInterceptor(jwtTokenUserInterceptor)
.addPathPatterns("/user/**")
.excludePathPatterns("/user/user/login")
.excludePathPatterns("/user/shop/status");
}
接口设计:
注意路径中第一个user是user表示用户端,然后第二个user代表的是用户模块。
返回的数据中data中id指的是数据库中这个用户的id,openid表示这个微信用户在小程序中的唯一标识。
首先是controller层:
需要写好接口,小程序端发送请求到这个接口,发送了授权码(code),然后服务端接收,接收之后调用service中的代码,通过httpclient发送请求到微信接口服务,使用封装好的httpclientutil发送get请求并携带appid、secret以及授权码等参数返回的结果是String类型的json数据,然后需要解析json数据得到openid,判断openid是否为空,为空登陆失败,不为空继续调用mapper中的根据openid查询代码,看是否为新用户,如果是新用户自动注册,调用insert方法添加到数据库,最终返回给controller层这个对象。然后controller层接收这个对象之后,需要给这个用户生成jwt令牌:
创建好jwt令牌之后,需要添加拦截器进行jwt验证,最后配置在webmvcconfiguration中。
@ApiOperation(value="员工登录")//添加说明
@PostMapping("/login")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
log.info("员工登录:{}", employeeLoginDTO);
Employee employee = employeeService.login(employeeLoginDTO);
//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);
EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token)
.build();
return Result.success(employeeLoginVO);
}
三、Redis的基本使用
本项目中使用到了redis的基本用法,主要是进行增删改查操作,并没有实现setnx加锁等稍微困难的操作,练手还是挺可以的。
Redis是一个基于内存的key-value结构数据库,其直接存储到内存中;mysql数据库是存储到磁盘中,查询操作是磁盘io操作。
下面是一些基础的启动操作:启用通过以下命令即可,这个是server端:
然后再打开一个命令行窗口,client端:使用这个命令会自动连接到本地的redis,如果想连接其他地方的redis,可以用下面的命令,其中-h表示ip地址,-p表示端口号:
下面介绍Redis中五种基本数据类型字符串 string
哈希hash
列表 list
集合set
有序集合zset(一般可以用来做排行榜相关)
常用命令:
字符串类型:
注意setnx一般当作锁来使用
哈希类型:
列表类型:
lpush lpop rpush rpop既可以模拟栈也可以模拟队列
集合类型:
有序集合:
通用命令:
测试如下:
Redis在java中的使用:
Spring Data Redis是Spring的一部分,对Redis底层开发进行了高度封装,在spring项目中,可以使用spring data redis来简化redis操作。
使用的操作步骤如下:
1.导入spring data redis的maven坐标:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
2.配置redis数据源
application-dev.yml中配置:
sky: datasource: driver-class-name: com.mysql.cj.jdbc.Driver host: localhost port: 3306 database: sky_take_out username: root password: dir99 redis: host: Localhost port: 6379 password: dir99 database: 1 wechat: appid: wx02bddf1e8f6f1036 secret: d3ed0a340ec836f1131f8f1b582531edy 引用配置:
3.编写配置类,创建RedisTemplate对象
@Configuration @Slf4j public class RedisConfiguration { @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){ log.info("开始创建redis模板对象..."); RedisTemplate redisTemplate=new RedisTemplate(); //设置redis的连接工厂对象: redisTemplate.setConnectionFactory(redisConnectionFactory); //设置redis key的序列化器,可以将key在redis可视化界面中显示出正常的字符串类型,不会出现看起来乱码的情况 redisTemplate.setKeySerializer(new StringRedisSerializer()); return redisTemplate; } }
下面是具体的使用:
如果一段时间内大量用户访问并且点菜,菜品都是存放在数据库中的,就要频繁访问数据库,会导致性能下降,卡顿,点击一个分类后可能要几秒钟之后才能显示出数据。所以可以使用缓存,把这些商品数据缓存到redis中,提高性能。
那么一个菜品加入一个缓存数据还是多个呢?可以想到小程序展示菜品的时候是根据分类来展示的,所以每一类保存一个缓存数据,每一个类构建一个key。
实现思路如下:
每一类可以保存一个缓存,在redis中是通过key-value来保存数据的,所以可以通过以下方式来保存,dish后面加一个动态数值,表示分类id,然后将java中list集合,转换为String类型(注意,此处java中的类型和redis中的类型不太一样,所以对list进行序列化然后转换成redis中的String类型)保存到redis中。但是如果菜品有变更,例如,管理端更改价格,要及时清理缓存数据,因为缓存没有同步更新过来,不清理的话会导致数据不一致性,所以要清理缓存数据。
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {
/**
* 使用缓存:
*/
//先构造key进行查询:dish_分类id格式:
String key="dish_"+categoryId;
//查询缓存中是否存在菜品数据 注意此处下方放进去是什么类型取出来就是什么类型,下面用的List<DishVO>,这里强转以下就行
List<DishVO> list = (List<DishVO>)redisTemplate.opsForValue().get(key);
if(list!=null&& list.size()>0){
//如果存在,直接返回,无需查询数据库
return Result.success(list);
}
//如果不存在,查询数据库,将查询的结果返回并且还要存到缓存中
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
list = dishService.listWithFlavor(dish);
redisTemplate.opsForValue().set(key,list);
return Result.success(list);
}
更改完之后,需要再继续更改,添加清理缓存操作,保证数据一致性:
分析当删除,新增,修改,起售停售菜品操作都需要清理缓存:
以上操作都在管理端进行,所以要在管理端进行修改:
清理缓存并不需要删除掉所有的缓存,只需要对更改的进行清理即可,哪一个分类被修改,清理这一个类即可。像新增操作改的哪个类删除哪个缓存数据即可,但是删除操作,前端传过来的是一个数组,里面可能包含多个菜品,这些菜品可能属于一个类也可能属于多个类,所以简单来看直接将所有的缓存删除即可。
注意修改操作:普通的修改像改名字,价格只设计一个分类,但是如果要更改分类的话,就要涉及两个分类,这一个类少一个那一个类多一个所以此处直接同样删除所有类的缓存数据。
起售停售菜品也是,前端传过来的有status以及id,如果想删除单个类的缓存也可以,需要根据菜品id查询分类进行查询操作然后得到这个类的key然后进行删除,但是需要进行查询操作有点浪费资源所以不如直接删除。
缓存套餐:
Spring cache(重要!!!)
有些类似AOP,只需要在需要缓存的方法加上注解即可。
在启动类上加上@EnableCaching注解来开启缓存注解功能。
如果想换为Caffeine只需要导入这个坐标,删除redis坐标即可。
后三个注解都是加在方法上的,cachePut与Cacheable的区别就是cacheable既可以取也可以存,另一个只能存不能取。
spring cache demo:
首先创建一个user表:
然后在pom文件中配置坐标,上图有;
@CachePut注解的使用
对于这个save方法,也就是插入操作,新增一个新用户,想插入数据库的同时保存到redis中一份,之前是通过redisTemplete然后set方法来实现,用了springcache之后,里面有一个注解就是****@CachePut注解****,可以直接将方法返回值加到缓存中。
#user.id,这个#后面的需要和方法中的形参名一致。但是要在mapper中使用主键回显!
在redis可视化软件中:
是根据冒号来确定的树形结构,完整的key是a:b:c:d
@Cacheable注解使用
对于这个getbyid方法,想在调用mapper之前先判断缓存中是否有这个数据,如果有的话直接返回缓存中的数据,如果没有的话,再去查询数据库返回。之前也是用的redisTemplete中get方法进行判断。先设置key然后get。使用SpringCache之后可以用****@Cacheable注解****。
加上注解之后:和上面的putCache注解类似,存到redis中的key为cacheNames::key,#
后面还需要和方法参数一致。
加上这个注解之后,其实在controller层的getById方法执行之前先有一个代理对象去缓存中查找,如果查找到了直接就返回缓存中的数据。这个方法其实没有被调用。如果没有查到通过反射来调用controller层中的这个方法。最后将返回结果放到redis中。
@CacheEvict注解的使用
这样可以删除一个缓存数据
这种方式将userCache下的键值缓存数据对全部都删除
得不偿失所以直接都删除掉。