本节,主要是学习业务逻辑,我们以菜品管理为例:
在实现这部分前,我们要完成Mybatis的配置,即指定映射的mapper.xml文件路径以及对应的实体类,这部分配置是在application.yml文件中实现的。
mybatis:#mapper配置文件mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.sky.entityconfiguration:#开启驼峰命名map-underscore-to-camel-case: true
这样配置后,在mapper.xml中如果不指定的话,接收的实体类就是com.sky.entity
中的,也就不需要每次都给指定接收类型了。
如下,我们没有指定传入的数据类型,因为在mapper接口中指定了,就可以省略。
<insert id="insert" useGeneratedKeys="true" keyProperty="id">insert into dish (name, category_id, price, image, description, create_time, update_time, create_user,update_user, status)values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime}, #{createUser},#{updateUser}, #{status})
</insert>
业务规则:
- 菜品名称必须是唯一的
- 菜品必须属于某个分类下,不能单独存在
- 新增菜品时可以根据情况选择菜品的口味
- 每个菜品必须对应一张图片
文件上传功能实现
我们需要用到的是阿里云对象存储服务:
阿里云OSS简介
阿里云对象存储服务(Object Storage Service,简称OSS)
为您提供基于网络的数据存取服务。使用OSS
,您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种非结构化数据文件。
阿里云OSS
将数据文件以对象(object)
的形式上传到存储空间(bucket)
中。
您可以进行以下操作:
- 创建一个或者多个存储空间,向每个存储空间中添加一个或多个文件。
- 通过获取已上传文件的地址进行文件的分享和下载。
- 通过修改存储空间或文件的属性或元信息来设置相应的访问权限。
- 在阿里云管理控制台执行基本和高级OSS任务。
- 使用阿里云开发工具包或直接在应用程序中进行
RESTful API
调用执行基本和高级OSS
任务
阿里云OSS开通与使用
那么我们具体该如何使用呢?进入阿里云官网:https://www.aliyun.com/
我们点击开通,这个是需要付费的。
开通成功:
进入控制台后,我们创建一个存储空间,即创建Bucket
,这个Bucket的名称是唯一的。
文件配置
首先是引入依赖:
<dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>3.15.1</version>
</dependency>
随后在application-dev.yml中进行配置,可以看到原本的配置为:
sky:alioss:endpoint: oss-cn-hangzhou.aliyuncs.comaccess-key-id: LTAI5tPeFLzsPPT8gG3LPW64access-key-secret: U6k1brOZ8gaOIXv3nXbulGTUzy6Pd7bucket-name: sky-take-out
我们把自己的写进去即可,这个是收费的,因此不要暴露
随后我们创建AccessKey
,并把值复制后我们的配置文件中。
至于其他的两个参数:
- endpoint:你选择杭州就是oss-cn-hangzhou.aliyuncs.com
你选择北京就是oss-cn-beijing.aliyuncs.com,可以理解吧 - bucket-name就是创建的名字(直接点击一下就可以复制啦)
随后在配置文件中读取:
spring:profiles:active: dev #设置环境
sky:alioss:endpoint: ${sky.alioss.endpoint}access-key-id: ${sky.alioss.access-key-id}access-key-secret: ${sky.alioss.access-key-secret}bucket-name: ${sky.alioss.bucket-name}
在sky-common
模块的properties
包中的 AliOssProperties
中,定义配置实体类:
/*** 阿里云配置实体类*/
@Component
/*请注意,为了使用@ConfigurationProperties注解,你需要在Spring Boot应用程序中启用配置绑定功能。
你可以通过在主应用程序类上添加@EnableConfigurationProperties注解来实现。*/
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {private String endpoint; //表示OSS服务的访问域名。private String accessKeyId; //表示访问OSS服务所需的Access Key ID。private String accessKeySecret; //表示访问OSS服务所需的Access Key Secret。private String bucketName; //表示要操作的存储桶名称。
生成OSS工具类对象
在sky-server模块的config包中进行配置,读取上面的配置信息,并返回一个操作对象。
/*** 配置类,用于创建AliOssUtil对象*/
@Configuration
@Slf4j
public class OssConfiguration {/*在这个配置类中,定义了一个名为aliOssUtil的@Bean方法,用于创建一个AliOssUtil对象。*/@Bean/*@ConditionalOnMissingBean注解表示当不存在名为aliOssUtil的bean时,才会创建该bean。这意味着如果已经有其他地方定义了名为aliOssUtil的bean,那么这个方法将不会执行。*/@ConditionalOnMissingBean //保证整个Spring容器中只有一个这个工具类/*在方法体中,通过依赖注入的方式获取AliOssProperties对象,并使用它的属性值来创建AliOssUtil对象。*/public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);return new AliOssUtil(aliOssProperties.getEndpoint(),aliOssProperties.getAccessKeyId(),aliOssProperties.getAccessKeySecret(),aliOssProperties.getBucketName());}
}
其中,AliOssUtil
类定义在sky-common
的utils
工具包中,改工具类将图像上传到阿里云OSS
后会生成一个访问地址,这个地址就是可以直接访问。
/*** AliOssUtil类是一个包含文件上传功能的工具类。*/
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {private String endpoint;private String accessKeyId;private String accessKeySecret;private String bucketName;/*** 文件上传** @param bytes* @param objectName* @return*/public String upload(byte[] bytes, String objectName) {// 创建OSSClient实例。/*在upload方法中,首先创建了一个OSSClient实例,用于与OSS服务进行交互。*/OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);try {// 创建PutObject请求。/*然后,通过调用ossClient.putObject方法将文件上传到指定的存储桶中。*/ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));/*在上传过程中,通过捕获OSSException和ClientException来处理可能出现的异常情况,并输出相应的错误信息。*/} catch (OSSException oe) {System.out.println("Caught an OSSException, which means your request made it to OSS, "+ "but was rejected with an error response for some reason.");System.out.println("Error Message:" + oe.getErrorMessage());System.out.println("Error Code:" + oe.getErrorCode());System.out.println("Request ID:" + oe.getRequestId());System.out.println("Host ID:" + oe.getHostId());} catch (ClientException ce) {System.out.println("Caught an ClientException, which means the client encountered "+ "a serious internal problem while trying to communicate with OSS, "+ "such as not being able to access the network.");System.out.println("Error Message:" + ce.getMessage());} finally {if (ossClient != null) {ossClient.shutdown();}}//文件访问路径规则 https://BucketName.Endpoint/ObjectName/*最后,构建文件的访问路径,并使用日志记录上传文件的路径。*/StringBuilder stringBuilder = new StringBuilder("https://");stringBuilder.append(bucketName).append(".").append(endpoint).append("/").append(objectName);//拼接路径返回到数据库里log.info("文件上传到:{}", stringBuilder.toString());return stringBuilder.toString();}
}
至此,阿里云OSS
的配置完成了。
最后,我们定义对应文件上传接口
/*** 通用接口*/
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
/*CommonController是一个使用AliOssUtil进行文件上传的控制器类。*/
public class CommonController {@Autowiredprivate AliOssUtil aliOssUtil;/*** 文件上传** @param file* @return*/@PostMapping("/upload")@ApiOperation("文件上传")public Result<String> upload(MultipartFile file) {log.info("文件上传:{}", file);try {//原始文件名/*首先通过file.getOriginalFilename()获取原始文件名*/String originalFilename = file.getOriginalFilename();//截取原始文件名的后缀 dfdfdf.png/*然后通过originalFilename.lastIndexOf(".")获取文件名的后缀。*/String extension = originalFilename.substring(originalFilename.lastIndexOf("."));//构造新文件名称/*使用UUID.randomUUID().toString()生成一个随机的文件名,并将后缀拼接在文件名后面,构造出新的文件名。*/String objectName = UUID.randomUUID().toString() + extension;//文件的请求路径/*然后,调用aliOssUtil.upload方法将文件上传到OSS,并获取文件的请求路径。*/String filePath = aliOssUtil.upload(file.getBytes(), objectName);/*最后,返回一个Result对象,其中包含上传文件的请求路径。*/return Result.success(filePath);} catch (IOException e) {log.error("文件上传失败:{}", e);}return Result.error(MessageConstant.UPLOAD_FAILED);}
}
随后我们在测试时发生报错:
nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to 127.0.0.1:6379] with root causejava.net.ConnectException: Connection refused: no further informationat sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) ~[na:1.8.0_201]
这个其实是由于我们没有启动Redis导致的,我们将Redis配置好后,启动服务即可。
使用了@Configuration
后,在服务启动过程中这些就会被加载执行。即这些类在服务启动过程中便会创建好对象从而方便我们使用。
菜品添加
完成文件上传功能后,这其实是一个公用接口,无论是菜品添加还是其他的文件上传都可以使用,随后,开始菜品添加模块的设计:
分析数据传输的组成,其有一个口味属性,这个属性中还包含许多属性:
因此,需要封装一个专门用于传递菜品数据的类:
package com.sky.dto;
import com.sky.entity.DishFlavor;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Data
public class DishDTO implements Serializable {private Long id;//菜品名称private String name;//菜品分类idprivate Long categoryId;//菜品价格private BigDecimal price;//图片private String image;//描述信息private String description;//0 停售 1 起售private Integer status;//口味private List<DishFlavor> flavors = new ArrayList<>();
}
随后定义Controller的添加菜品接口
@PostMapping()@ApiOperation("新增菜品")public Result save(@RequestBody DishDTO dishDTO) {log.info("新增菜品:{}", dishDTO);dishService.saveWithFlavor(dishDTO);// 清理缓存数据String key = "dish_" + dishDTO.getCategoryId();clearCache(key);return Result.success();}
定义Service的操作,这里涉及到两个表,分别是菜品表与口味表,因此需要开启事务管理,同时由于口味可以有多个,因此可以使用SQL的动态拼接来插入多条数据:
@Override@Transactional//由于涉及两张表,因此需要开启事务,即全成功或者全失败public void saveWithFlavor(DishDTO dishDTO) {Dish dish = new Dish();BeanUtils.copyProperties(dishDTO, dish);//Spring提供的属性拷贝// 向菜品表插入1条数据dishMapper.insert(dish);// 获取insert语句生成的主键值Long dishId = dish.getId();List<DishFlavor> flavors = dishDTO.getFlavors();if (flavors != null && flavors.size() > 0) {flavors.forEach(dishFlavor -> dishFlavor.setDishId(dishId));
// 向口味表插入n条数据,口味有多种dishFlavorMapper.insertBatch(flavors);}}
关于上面代码中的forEach,其可以实现遍历赋值。
TreeSetTest t1= new TreeSetTest("李白",19);
TreeSetTest t2= new TreeSetTest("杜甫",18);
Set<TreeSetTest> s=new TreeSet<>((o1, o2) -> o2.getAge()-o1.getAge());
s.add(t1);
s.add(t2);
s.forEach(ss->ss.setAge(11));//这两个for循环语句作用相同,都能够改变s的值,即修改里面的值
for (TreeSetTest ss:s) {ss.setAge(11);
}
编写批量插入的SQL
语句,在这段语句中,collection
的参数就是口味list
,每个对象用item
代表,这个随便起,保存与下面的对象一致即可,separator=","
是用于在每个 (#{item.dishId},#{item.name},#{item.value})
后面加一个逗号表示分割。
<insert id="insertBatch">insert into dish_flavor (dish_id, name, value) VALUES<foreach collection="flavors" separator="," item="item">(#{item.dishId},#{item.name},#{item.value})</foreach>
</insert>
这里还需要注意的是,我们需要获取到菜品的id,而此时我们是在添加菜品,在菜品对象中是没有id
的,因此可以使用insert
插入语句中的返回值
useGeneratedKeys="true"
代表需要主键值, keyProperty="id"
表示将主键值赋给菜品对象的id
属性
<insert id="insert" useGeneratedKeys="true" keyProperty="id">insert into dish (name, category_id, price, image, description, create_time, update_time, create_user,update_user, status)values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime}, #{createUser},#{updateUser}, #{status})</insert>
如业务层中所编写的那样,在执行完insert
后就可以从对象中获取到属性值了。
// 向菜品表插入1条数据
dishMapper.insert(dish);
// 获取insert语句生成的主键值
Long dishId = dish.getId();
//使用forEach将口味对象的菜品id赋值
flavors.forEach(dishFlavor -> dishFlavor.setDishId(dishId));
菜品分页查询
根据文档分析,菜品查询所接收的参数为:
因此定义DishPageQueryDTO
package com.sky.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class DishPageQueryDTO implements Serializable {private int page;private int pageSize;private String name;//分类idprivate Integer categoryId;//状态 0表示禁用 1表示启用private Integer status;
}
此外我们看到上面的实体类后面实现了一个Serializable接口,这样做的意义是什么呢?
为何要implements Serializable ?
Controller层定义:
@GetMapping("/page")@ApiOperation("菜品分页查询")public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO) {log.info("菜品分页查询:{}", dishPageQueryDTO);PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);//后绪步骤定义return Result.success(pageResult);}
基于PageHelper实现分页查询的业务层代码:
@Overridepublic PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());Page<DishVO> page=dishMapper.pageQuery(dishPageQueryDTO);return new PageResult(page.getTotal(), page.getResult());}
这里需要注意的是返回结果是DishVO
,因为结果涉及两个表的联合查询,要查类别名称。
<select id="pageQuery" resultType="com.sky.vo.DishVO">select d.*,c.name as categoryNamefrom dish d left join category c on d.category_id = c.id<where><if test="name!=null">and d.name like concat('%',#{name},'%')</if><if test="categoryId!=null">and d.categoryId like concat('%',#{categoryId},'%')</if><if test="status!=null">and d.status like concat('%',#{status},'%')</if></where>order by d.create_time desc</select>
至此,完成菜品的分页查询功能。
菜品删除
业务规则:
- 可以一次删除一个菜品,也可以批量删除菜品
- 起售中的菜品不能删除
- 被套餐关联的菜品不能删除
- 删除菜品后,关联的口味数据也需要删除掉
根据上面的业务逻辑,这将涉及三个表:菜品表,口味表,套餐与菜品关系表
Controller
层定义,前端发来的请求是?参数名=参数值
的形式,ids原本为字符串类型,即1,3,5,7,8
,我们希望将这种数据转换为List
列表这种形式,我们可以自己处理,但SpringMVC
为我们提供了一种方法,即在前面加一个@RequestParam
注解即可将其转换为List
形式。
@DeleteMapping@ApiOperation("菜品批量删除")public Result delete(@RequestParam List<Long> ids) {log.info("菜品批量删除:{}", ids);dishService.deleteBatch(ids);// 将所有的菜品缓存数据清理掉,所有以dish_开头的keyclearCache("dish_*");return Result.success();}
随后Service
层定义业务逻辑,要求删除菜品时需检查在套餐中是否含有,还要检查是否在售,在删除时要连同菜品口味一并删除。
@Override@Transactionalpublic void deleteBatch(List<Long> ids) {
// 判断当前菜品是否能够删除---是否存在起售中的菜品??ids.forEach(id->{Dish dish = dishMapper.getById(id);if (dish.getStatus() == StatusConstant.ENABLE) {
// 当前菜品处于起售中,不能删除throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);}});// 判断当前菜品是否能够删除---是否被套餐关联了??List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);if (setmealIds != null && setmealIds.size() > 0) {
// 当前菜品被套餐关联了,不能删除throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);}// 删除菜品表中的菜品数据ids.forEach(id->{dishMapper.deleteById(id);// 删除菜单关联的口味数据dishFlavorMapper.deleteByDishId(id);});}
如果被套餐关联或者在售卖中,则会抛出异常,抛出异常后这个异常就会被捕获,因为这些异常都是BaseException
的子类,因此都可以在全局异常处理中被捕获:
package com.sky.exception;
public class DeletionNotAllowedException extends BaseException {public DeletionNotAllowedException(String msg) {super(msg);}
}
/*** 全局异常处理器,处理项目中抛出的业务异常*/
@RestControllerAdvice//开启异常捕获
@Slf4j
public class GlobalExceptionHandler {/*** 捕获业务异常* @param ex* @return*/@ExceptionHandlerpublic Result exceptionHandler(BaseException ex){log.error("异常信息:{}", ex.getMessage());return Result.error(ex.getMessage());}
}
查看菜品是否与套餐相关联,原始的SQL语句
select setmeal_id form setmeal_dish where dish_id in (1,3,5,8)
Mybatis中的动态SQL:
<select id="getSetmealIdsByDishIds" resultType="java.lang.Long">select setmeal_idfrom setmeal_dish where dish_id in<foreach collection="ids" separator="," item="item" open="(" close=")">#{item}</foreach></select>
至此,菜品删除便完成了,但我们发现一个问题,在删除菜品时,如果id过多,则会执行多次删除语句,这样势必对性能会造成影响,那么我们可以考虑使用批量删除语句:
delete from tables where id in (1,3,5,6,7)
// 删除菜品表中的菜品数据
// ids.forEach(id->{
// dishMapper.deleteById(id);
//
删除菜单关联的口味数据
// dishFlavorMapper.deleteByDishId(id);
// });//批量删除菜品dishMapper.deleteByIds(ids);dishFlavorMapper.deleteByDishIds(ids);
对应的动态删除xml配置:
<delete id="deleteByIds">deletefrom dish where id in<foreach collection="ids" item="id" separator="," open="(" close=")">#{id}</foreach></delete>
<delete id="deleteByDishIds">deletefrom dish_flavor where dish_id in<foreach collection="ids" item="dishid" separator="," open="(" close=")">#{dishid}</foreach></delete>
菜品修改
菜品修改涉及到的接口有:
- 根据id查询菜品
- 上传文件
- 查询菜品类别
- 修改菜品
其中,上传文件与查询菜品类别我们已经完成了,接下来便是根据id查询菜品和修改菜品了。
首先定义要返回的数据类型:
package com.sky.vo;import com.sky.entity.DishFlavor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DishVO implements Serializable {private Long id;//菜品名称private String name;//菜品分类idprivate Long categoryId;//菜品价格private BigDecimal price;//图片private String image;//描述信息private String description;//0 停售 1 起售private Integer status;//更新时间private LocalDateTime updateTime;//分类名称private String categoryName;//菜品关联的口味private List<DishFlavor> flavors = new ArrayList<>();//private Integer copies;
}
Controller层接口实现:
@GetMapping("/{id}")
@ApiOperation("根据id查询菜品")
public Result<DishVO> getById(@PathVariable Long id) {log.info("根据id查询菜品:{}", id);DishVO dishVO = dishService.getByIdWithFlavor(id);return Result.success(dishVO);
}
业务层代码
@Overridepublic DishVO getByIdWithFlavor(Long id) {
// 根据id查询菜品数据Dish dish = dishMapper.getById(id);// 根据菜品id查询口味数据List<DishFlavor> dishFlavorList = dishFlavorMapper.getByDishId(id);// 将查询到的数据封装到voDishVO dishVO = new DishVO();BeanUtils.copyProperties(dish, dishVO);dishVO.setFlavors(dishFlavorList);return dishVO;}
Mybatis 对应的SQL语句:
<select id="getById" resultType="com.sky.entity.Dish">select *from dish where id=#{id};</select>
随后开始修改操作的接口开发,该修改涉及菜品表与口味表:
@PutMapping@ApiOperation("修改菜品")public Result update(@RequestBody DishDTO dishDTO) {log.info("修改菜品:{}", dishDTO);dishService.updateWithFlavor(dishDTO);//将所有的菜品缓存数据清理掉,所有以dish_开头的keyclearCache("dish_*");return Result.success();}
由于涉及口味的修改,而我们可能会对口味进行增删操作,这还要判断,因此可以进行先全部删除,再将现有口味重新添加的方式来实现,即逻辑上是修改,技术实现上是删除与添加。
@Overridepublic void updateWithFlavor(DishDTO dishDTO) {Dish dish = new Dish();BeanUtils.copyProperties(dishDTO, dish);// 修改菜品基本信息dishMapper.update(dish);// 删除原有的口味信息dishFlavorMapper.deleteByDishId(dishDTO.getId());// 重新插入口味数据List<DishFlavor> flavors = dishDTO.getFlavors();if (flavors != null && flavors.size() > 0) {flavors.forEach(dishFlavor -> dishFlavor.setDishId(dishDTO.getId()));// 向口味表插入n条数据dishFlavorMapper.insertBatch(flavors);}
菜品修改动态SQL
<update id="update">update dish<set><if test="name != null">name = #{name},</if><if test="categoryId != null">category_id = #{categoryId},</if><if test="price != null">price = #{price},</if><if test="image != null">image = #{image},</if><if test="description != null">description = #{description},</if><if test="status != null">status = #{status},</if><if test="updateTime != null">update_time = #{updateTime},</if><if test="updateUser != null">update_user = #{updateUser},</if></set>where id =#{id}</update>
批量删除与插入口味信息
<delete id="deleteByDishIds">deletefrom dish_flavor where dish_id in<foreach collection="ids" item="dishid" separator="," open="(" close=")">#{dishid}</foreach></delete>
<insert id="insertBatch">insert into dish_flavor (dish_id, name, value) VALUES<foreach collection="flavors" separator="," item="item">(#{item.dishId},#{item.name},#{item.value})</foreach>
</insert>
至此,便完成了菜品信息的增删改查了。