一、前言
上一节将员工的CRUD做出来了,同时由于步骤几乎相同,对于分类的Controller,我们直接导入,就不重复书写了,接下来就要做菜品的CRUD了,这里会使用到阿里云OSS来存储文件(图片),同时菜品有不同的口味选择,所以需要两个表存储。
二、通用接口---文件上传
对于文件上传部分遗忘的,可以在这一篇文章中看到:
SpringMVC ------ 响应和请求处理-CSDN博客
通用接口中将实现功能实现中公共的方法,这里我们先只添加文件上传的方法。
文件上传的原理就是通过阿里云OSS来实现云存储,这样可以方便后续菜品的图片上传的存储。
先看看文档怎么描述的,很显然,是通过请求体传入一个


依旧从上往下书写,先写通用接口的Controller,里面内含文件上传的方法:
java
/**
* 通用接口
*/
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {
//自动装配的是OssConfiguration中创建的含参数的aliOssUtil对象
@Autowired
private AliOssUtil aliOssUtil;
/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
@ApiOperation("文件上传")
public Result<String> upload(MultipartFile file) {
log.info("文件上传:{}",file);
try {
//原始文件名
String originalFilename = file.getOriginalFilename();
//截取原始文件名的后缀 dfdfdf.pgn
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
//构建新文件名称
String objectName = UUID.randomUUID().toString() + extension;
//文件的请求路径
String filePath = aliOssUtil.upload(file.getBytes(), objectName);
return Result.success(filePath);
} catch (IOException e) {
log.error("文件上传失败:{}",e);
}
return Result.error(MessageConstant.UPLOAD_FAILED);
}
}
对于这个方法,我们的目的是通过工具类将指定文件上传到阿里云,我们从请求体中接收一个MultipartFile(二进制的文件类型参数),若上传成功,最后将返回一个文件路径的字符串到data,若上传失败,将返回报错结果集到msg。
这里对于文件名是进行了处理的,使用的是UUID来对文件进行随机命名(结合多种元素如时间戳、随机数等),但是由于我们依旧需要扩展名,所以要先将后缀分离出来,然后将文件名部分处理,最后拼接在一起。
最终得到类似的文件名:

OSS的工具类如下,这是基于阿里云官网给出的Java文档进行封装的,还是比较简单的:
java
@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实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
} 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的自动装配的配置类如下,目的是创建aliOssUtil的bean,便于在接口中自动装配:
java
/**
* 用于创建AliOssUtil对象
*/
@Configuration
@Slf4j
public class OssConfiguration {
@Bean
@ConditionalOnMissingBean//只要没有这个Bean就创建
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
log.info("开始创建阿里云文件上传工具对象{}",aliOssProperties);
return new AliOssUtil(
aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}
三、菜品的CRUD
1.新增菜品
(1)文档
老样子,先看文档:

(2)Controller
可以看到这里需要接收请求体中的参数,所以直接就能想到用DTO接收,并且用@RequestBody标记,所以很容易可以写出:
java
/**
* 菜品管理
*/
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {
@Autowired
private DishService dishService;
/**
* 新增菜品
* @param dishDTO
* @return
*/
@PostMapping()
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishDTO){
log.info("新增菜品:{}",dishDTO);
dishService.saveWithFlavor(dishDTO);
return Result.success();
}
}
DTO如下:
java
@Data
public class DishDTO implements Serializable {
private Long id;
//菜品名称
private String name;
//菜品分类id
private Long categoryId;
//菜品价格
private BigDecimal price;
//图片
private String image;
//描述信息
private String description;
//0 停售 1 起售
private Integer status;
//口味
private List<DishFlavor> flavors = new ArrayList<>();
}
值得注意的是,这里我们使用了一个数组集合来储存口味选项,因为一个菜品的口味可能有很多个,所以自然的,我们需要一个新的表来存储这个数据,这个表中由dish_id来作为逻辑外键,与dish表的主键进行关联(这样就知道哪几个口味属于哪一个菜品了):

同时dish也需要一个表来存储:

(3)Service层
接下来看Service层:
接口:
java
public interface DishService {
/**
* 新增菜品
* @param dishDTO
*/
public void saveWithFlavor(DishDTO dishDTO);
}
实现类:
java
@Service
@Slf4j
public class DishServiceImp implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
@Override
@Transactional
public void saveWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
dishMapper.insert(dish);
//获取Insert语句生成的主键值
Long dishId = dish.getId();
//向口味表中插入n条数据
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() > 0){
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishId);
});
//向口味表插入n条数据
dishFlavorMapper.insertBatch(flavors);
}
}
}
这里需要注意了,这个与员工的新增就不太一样了,首先先插入dish到菜品表中,这一点是一样的。
但是这里我们需要额外处理口味表:由于是用一个数组集合存储口味的,所以这里需要遍历口味表来将每个口味对应的dish_id设置为当前插入的菜品的id(逻辑外键),最终才将这些口味插入到口味表中去。
对应关系可以看看下图:

(4)持久层
两个表的mapper如下:
java
@Mapper
public interface DishFlavorMapper {
/**
* 批量插入口味数据
* @param flavors
*/
void insertBatch(List<DishFlavor> flavors);
}
java
@Mapper
public interface DishMapper {
/**
* 根据分类id查询菜品数量
* @param categoryId
* @return
*/
@Select("select count(id) from dish where category_id = #{categoryId}")
Integer countByCategoryId(Long categoryId);
/**
* 新增菜品和对应口味
* @param dish
*/
@AutoFill(value = OperationType.INSERT)
void insert(Dish dish);
}
由于新增操作涉及插入的变量较多,我们就不使用注解了,这里使用xml来配置:
XML
<mapper namespace="com.sky.mapper.DishFlavorMapper">
<insert id="insertBatch" >
insert into dish_flavor(dish_id, name, value) VALUES
<foreach collection="flavors" item="df" separator=",">
(#{df.dishId},#{df.name},#{df.value})
</foreach>
</insert>
</mapper>
这里是用了动态sql语句的,将flavors集合遍历,插入表中(先前已经在Service层中处理了逻辑外键问题了)
XML
<mapper namespace="com.sky.mapper.DishMapper">
<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>
</mapper>
这里有两个参数比较陌生:
1. useGeneratedKeys="true":
作用:告知 MyBatis,当前插入操作的主键是由数据库自动生成的(例如 MySQL 的 AUTO_INCREMENT)
2. keyProperty="id":
作用:指定将数据库生成的主键值,赋值到 Java 对象的哪个属性上。
2.分页查询菜品
分页查询依旧需要使用到PageHelper。
(1)文档
先观察文档

很容易发现分页查询是用的Query参数,这就代表需要用到分页插件了,返回值是查询到的内容。
(2)Controller
有了员工的查询经验,这里很容易写出菜品的分页查询,这里由于使用到了分页插件,所以需要专门创建一个DTO来存储数据,Controller内容很简单,先日志,然后调用Service层,最后返回结果集,由于我们需要分好了页的结果,所以这里传到结果集中的是pageResult对象。
java
/**
* 菜品分页查询
* @param dishPageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO) {
log.info("菜品分页查询:{}", dishPageQueryDTO);
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}
DTO设计如下:
java
@Data
public class DishPageQueryDTO implements Serializable {
private int page;
private int pageSize;
private String name;
//分类id
private Integer categoryId;
//状态 0表示禁用 1表示启用
private Integer status;
}
(3)Service层
接口:
java
/**
* 菜品分页查询
* @param dishPageQueryDTO
* @return
*/
PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);
**实现类:**实现类中调用了PageHelper插件,传入DTO,这个DTO其实是前端发送过来的请求体,这里面包含了很多参数,包括了页码和每页记录数(所以这个DTO也与普通的DTO不一样),这将作为startPage方法传入的参数用于开启分页。
然后我们需要调用持久层获取查询结果,结果是用VO封装的(VO是前端展示数据,DTO是前端请求后端的),最终我们返回的VO结果集还需要封装到我们自己创建的PageResult中去,然后传给Controller。
其实这里可以理解为当我们按下下一页按钮时,前端就会重新发出一个请求,这时新DTO的参数就会改变成当前页的,于是开启分页时的参数也改变了,当然从持久层拿出的结果VO也会变,于是封装VO的结果集也变了,我们自己封装结果集的PageResult当然也会变,最后的结果就是在Controller中传回的响应结果也变了(响应回去的data中的数据),于是响应回前端的数据就变成当前页的了。
java
/**
* 菜品分页查询
*
* @param dishPageQueryDTO
* @return
*/
@Override
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
return new PageResult(page.getTotal(), page.getResult());
}
我们自己封装的分页查询结果集如下:
java
/**
* 封装分页查询结果
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {
private long total; //总记录数
private List records; //当前页数据集合
}
(4)持久层
前面也提到了,持久层在分页中的作用就是拿出VO结果,我们要清晰的知道目的,不然会被各种封装绕晕。
**Mapper:**由于使用到动态语句,所以这里还是使用xml文件配置。
java
/**
* 菜品分页查询
* @param dishPageQueryDTO
* @return
*/
Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);
映射文件:
这里使用的语句就是外连接语句了。
可以到这篇文章中复习一下:
这里的sql语句很复杂,先要搞清楚目的,再看这条语句就会觉得豁然开朗了。
这里的目的是:通过动态条件查询 dish 表的菜品信息,并关联(通过id关联) category 表获取菜品对应的分类名称(通过外连接),最终将结果封装到 DishVO 实体类中,支持按名称模糊查询、按分类 ID 筛选、按状态筛选,并按创建时间倒序排列(最新创建的菜品在前)
java
<select id="pageQuery" 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="categoryId != null">
and d.category_id = #{categoryId}
</if>
<if test="status != null">
and d.status = #{status}
</if>
</where>
order by d.create_time desc
</select>
3.批量删除菜品
(1)文档
先看文档:

这里请求的参数是**ids,**而且是String类型的,这样我们很不好处理,好在Spring很智能,可以将这个请求参数自动转化为一个集合,所以这里我们必然会使用到@RequestParam注解,返回值没有要求,就是返回个成功结果集就行了。
(2)Controller
java
/**
* 菜品批量删除
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids){
log.info("菜品批量删除:{}",ids);
dishService.deleteBatch(ids);
return Result.success();
}
没什么好说的,用了刚刚的注解,将请求参数转换成了集合。
(3)Service层
接口:
java
/**
* 菜品批量删除
* @param ids
*/
void deleteBatch(List<Long> ids);
实现类:
这里要说一下,我们的删除和批量删除是做到一起的,因为批量删除完全可以完成删除的功能,完全可以复用。
来看具体的要求:起售的菜品肯定不能删,和套餐关联的菜品肯定也不能删,条件都满足,就可以删,所以我们采用了以下的逻辑:
遍历前端传来的集合(转换后),每次循环都从持久层拿一个对应id的对象,判断起售状态,不满足就抛异常,满足就继续判断是否被套餐关联。
然后去套餐表中查是否有这个id,如果有就抛异常,没有就进行删除操作。
删除操作是需要循环的,确保每个id对应的菜品都被删除。
java
/**
* 菜品批量删除
*
* @param ids
*/
@Override
@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);
}
}
//判断当前菜品是否能够删除---是否被套餐关联了
List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
if (setmealIds != null && setmealIds.size() > 0) {
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
//删除菜品表中的菜品数据
for (Long id : ids) {
dishMapper.deleteById(id);
//删除口味数据
dishFlavorMapper.deleteByDishId(id);
}
}
(4)持久层
下面两个简单,直接上注解,一个是用id查菜品,一个是用id删菜品。
java
/**
* 根据主键查询菜品
* @param id
* @return
*/
@Select("select * from dish where id = #{id}")
Dish getById(Long id);
java
/**
* 根据主键删除菜品数据
* @param id
*/
@Delete("delete from dish where id = #{id}")
void deleteById(Long id);
但是从套餐中查id就不能直接用注解了,因为需要动态sql语句,我们需要遍历套餐表,去查找在中间表中,是否对应的菜品id有对应的套餐id。
java
/**
* 根据id查询对应套餐id
* @param dishIds
* @return
*/
//select setmeal_id from setmeal dish where dish_id in (1,2,3,4)
List<Long> getSetmealIdsByDishIds(List<Long> dishIds);
目的:通过传入的多个菜品 ID(dishIds),查询出所有包含这些菜品的套餐 ID(setmeal_id),即找出哪些套餐关联了指定的菜品。
XML
<mapper namespace="com.sky.mapper.SetmealDishMapper">
<select id="getSetmealIdsByDishIds" resultType="java.lang.Long">
select setmeal_id from setmeal_dish where dish_id in
<foreach collection="dishIds" item="dishId" separator="," open="(" close=")">
#{dishId}
</foreach>
</select>
</mapper>
这三个表关系如图:

(5)优化
最后一步删除操作是可以优化的,将循环在数据库中进行,节省从Java到数据库中消耗的时间。
可以优化如下:
java
//优化
//根据菜品id集合批量删除菜品数据
//sql: delete from dish where id in (?,?,?)
dishMapper.deleteByIds(ids);
//根据菜品id集合批量删除关联的口味数据
//sql: delete from dish_flavor where dish_id in (?,?,?)
dishFlavorMapper.deleteByDishIds(ids);
分别在菜品和口味的Mapper中添加优化的查找方式(在sql中动态循环):
java
/**
* 根据菜品id集合批量删除菜品
* @param ids
*/
void deleteByIds(List<Long> ids);
XML
<delete id="deleteByIds">
delete from dish where id in
<foreach collection="ids" open="(" close=")" separator="," item="id">
#{id}
</foreach>
</delete>
java
/**
* 根据菜品id集合批量删除关联的口味数据
* @param dishids
*/
void deleteByDishIds(List<Long> dishids);
XML
<delete id="deleteByDishIds" >
delete from dish_flavor where dish_id
<foreach collection="dishIds" open="(" close=")" separator="," item="dishId">
#{dishId}
</foreach>
</delete>
4.修改菜品
没有什么要注意的,别忘记修改时选项要回显就行(相当于又是一个接口,查询接口)。
(1)文档

修改操作注意需要做到数据的回显,所以可以看到请求体中向后端带来了全部参数,最终只需要返回成功结果集即可。
(2)Controller
用DTO接收请求体中的数据,然后调用Service层。
java
/**
* 修改菜品
* @param dishDTO
* @return
*/
@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishDTO){
log.info("修改菜品");
dishService.updateWithFlavor(dishDTO);
return Result.success();
}
(3)Service层
接口:
java
/**
* 根据id修改菜品和口味信息
* @param dishDTO
*/
void updateWithFlavor(DishDTO dishDTO);
实现类:
java
/**
* 修改信息
* @param dishDTO
*/
@Override
public 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);
}
}
(4)持久层
Mapper:
java
/**
* 根据id动态修改菜品
* @param dish
*/
@AutoFill(value = OperationType.UPDATE)
void update(Dish dish);
映射文件:
XML
<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>
(5)回显
原理是点下修改按钮,就会跳转页面,同时向后端发起请求参数id,后端根据id查询菜品和口味,最后从后端返回VO集合(由于是要向前端展示的,所以用的VO)。
虽然也算一个完整接口,但是很简单,所以给出代码即可。
java
/**
* 根据id查询菜品
* @param id
* @return
*/
@GetMapping("/{id}")
public Result<DishVO> getById(@PathVariable Long id){
log.info("根据id查询菜品:{}",id);
DishVO dishVO = dishService.getByIdWithFlavor(id);
return Result.success(dishVO);
}
java
/**
* 根据id查询菜品和对应口味
* @param id
* @return
*/
DishVO getByIdWithFlavor(Long id);
java
/**
* 根据id查询菜品和对应口味
* @param id
* @return
*/
@Override
public DishVO getByIdWithFlavor(Long id) {
//根据id查询菜品数据
Dish dish = dishMapper.getById(id);
//根据菜品id查询口味数据
List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id);
//将查询到的数据封装到VO
DishVO dishVO = new DishVO();
BeanUtils.copyProperties(dish,dishVO);
dishVO.setFlavors(dishFlavors);
return dishVO;
}
java
/**
* 根据主键查询菜品
* @param id
* @return
*/
@Select("select * from dish where id = #{id}")
Dish getById(Long id);