新增套餐
在进行业务开发前,我们要先研究一下接口文档
首先要明确该功能的请求路径以及请求方式,该功能是POST请求。
参数是放在请求体当中,意味着我们在控制层接收参数时要用@RequestBody注解。同时我们一定要注意,从接口文档里可以看出套餐里是附有若干个菜品的,因此我们新增套餐,不仅仅是新增套餐,也是新增菜品。通过对数据库表之间关系梳理,套餐里面的菜品是存入setmeal_dish表里,而不是dish表!
我们在controller.admin包下创建一个SetmealController的java类,即为负责套餐管理模块的控制层。在控制层,我们先写上一些必要的注解,
@RestController
@RequestMapping("/admin/setmeal")
@Api(tags = "套餐相关接口")
@Slf4j
public class SetmealController {
}
接下来在service包下创建一个服务层的接口
public interface SetmealService {
}
在impl包下创建一个java类来实现此接口:
@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {
}
别忘了在这个实现类里加上注解@Service,告诉Spring这里是负责服务层的代码。
在mapper包下创建SetmealMapper
@Mapper
public interface SetmealMapper {
}
同样别忘了@Mapper注解。这些必要的准备工作做好后,我们就可以开始写代码了。
首先在控制层中:
SetmealController.java
我们先从spring容器中注入SetmealService对象
@Autowired
private SetmealService setmealService;
接下来我们就接收前端的请求:
@PostMapping
@ApiOperation("新增套餐")
public Result save(@RequestBody SetmealDTO setmealDTO) {
log.info("新增套餐:{}",setmealDTO);
setmealService.saveWithDish(setmealDTO);
return Result.success();
}
其中,第一行的注释@PostMapping表明负责前端的POST请求,@ApiOperation是Swagger的注解。由于新增套餐该功能不需要返回数据库的数据,因此该方法的返回类型为Result。我们用SetmealDTO来接收前端传来的数据,并在参数前面加上@RequestBody注解。我们加一句日志后,只需要直接调用服务层的方法即可。
SetmealServiceImpl.java
在对应的SetmealService接口要用有方法
//新增套餐和相应的菜品
void saveWithDish(SetmealDTO setmealDTO);
接下来我们就在实现类里重写该方法:
@Override
@Transactional
public void saveWithDish(SetmealDTO setmealDTO) {
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO,setmeal);
//向套餐插入数据
setmealMapper.insert(setmeal);
Long setmealId = setmeal.getId();
//向菜品插入n条数据
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
if (setmealDishes != null && setmealDishes.size() > 0 ){
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);
});
setmealDishMapper.insertBatch(setmealDishes);
}
}
(以上代码是该方法的完整代码)
由于涉及到两张表的操作,我们在方法前面加一个@Transactional事务注解,保证两个表的操作能够同时成功或同时失败,而不至于拖泥带水。前面说到,新增套餐其实是两个操作,将套餐本身的信息插入setmeal表,还有将相应的菜品插入setmeal_dish表。所以我们创建一个Setmeal对象,将setmealDTO的属性通过BeanUtils.copyProperties()方法拷贝给setmeal,这样我们直接将setmeal对象作为参数传给操作setmeal数据库的持久层。
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO,setmeal);
//向套餐插入数据
setmealMapper.insert(setmeal);
要注意,在setmeal_dish表中有字段名:setmeal_id。因此我们要获得setmeal的id。接下来我们完成向setmeal_dish表插入菜品的操作。因为一个套餐里有若干个菜品,所以我们用List集合来封装菜品:
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
然后我们遍历List集合,把setmealId赋值给每一个菜品(强调一下套餐里的菜品setmealDish和菜品Dish是两回事,不要混淆!)
if (setmealDishes != null && setmealDishes.size() > 0 ){
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);
});
赋值后我们就可以把菜品插入setmeal_dish表里了:
setmealDishMapper.insertBatch(setmealDishes);
至此服务层的代码我们写好了。
接下来看持久层
SetmealMapper.java
@AutoFill(value = OperationType.INSERT)
void insert(Setmeal setmeal);
这里是插入套餐对应的持久层方法,由于涉及到的字段比较多,因此我们选择在xml文件里编写SQL语句,同时我们加上注解@AutoFill注解(这是自定义注解,在此不多赘述),为公共字段自动填充。
SetmealMapper.xml
<insert id="insert" parameterType="Setmeal" useGeneratedKeys="true" keyProperty="id">
insert into setmeal(category_id, name, price, description, image, create_time, update_time, create_user, update_user)
values
(#{categoryId},#{name},#{price},#{description},#{image},#{createTime},#{updateTime},#{createUser},#{updateUser})
</insert>
在这里要特别强调的是一定要加上useGeneratedKeys="true" keyProperty="id"。否则在服务层获取setmeal的id时就不能获取id,作者曾在这里卡了很久。
接下来我们看操作setmeal_dish表的持久层
SetmealDishMapper.java
void insertBatch(List<SetmealDish> setmealDishes);
由于涉及到动态SQL语句,所以我们依然在xml文件里编写SQL语句
SetmealDishMapper.xml
<insert id="insertBatch">
insert into setmeal_dish(setmeal_id, dish_id, name, price, copies) values
<foreach collection="setmealDishes" item="setmealDish" separator=",">
(#{setmealDish.setmealId},#{setmealDish.dishId},#{setmealDish.name},#{setmealDish.price},#{setmealDish.copies})
</foreach>
</insert>
因为需要插入表里的菜品有若干个,所以我们要用<foreach>标签将这些菜品一个个插入数据库中。
至此新增套餐的功能已完成。
套餐分页查询
分页查询和视频里之前做的菜品的分页查询区别不大,我们就不多讲述了。作者仅在需要注意的小坑再强调二三。
首先看一看接口文档,明确请求方式、请求路径、参数类型等
再看看返回数据格式
说明我们需要把数据库里的数据返回给前端。
SetmealController.java
@GetMapping("/page")
@ApiOperation("套餐分页查询")
public Result<PageResult> pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {
log.info("套餐分页查询:{}",setmealPageQueryDTO);
PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
log.info("分页查询结果:{}",pageResult);
return Result.success(pageResult);
}
分页查询我们同样需要用到PageHelper插件,我们用SetmealPageQueryDTO类型来接收前端的请求参数。然后我们直接调用服务层即可,然后把pageResult返回给前端。
SetmealService.java
//套餐分页查询
PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);
然后在实现类里重写该方法:
@Override
public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {
PageHelper.startPage(setmealPageQueryDTO.getPage(), setmealPageQueryDTO.getPageSize());
Page<SetmealVO> page = setmealMapper.pageQuery(setmealPageQueryDTO);
return new PageResult(page.getTotal(), page.getResult());
}
PageHelper分页需要两个参数,一个是页码,一个是单页展示数据总数,而这些在我们接收的前端数据里已经有了,所以我们对setmealPageQueryDTO调用getPage()和getPageSize方法即可。
因为查询出来的数据是多条的,所以我们用集合来接收持久层查出来的数据
Page<SetmealVO> page = setmealMapper.pageQuery(setmealPageQueryDTO);
服务层的代码完毕后,我们看持久层
SetmealMapper.java
Page<SetmealVO> pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);
SetmealMapper.xml
我们在xml文件里编写SQL语句:
<select id="pageQuery" resultType="com.sky.vo.SetmealVO">
select setmeal.*,category.name as categoryName from setmeal left join category on
setmeal.category_id = category.id
<where>
<if test="name != null ">
and setmeal.name like concat('%',#{name},'%')
</if>
<if test="categoryId != null">
and setmeal.category_id = #{categoryId}
</if>
<if test="status != null">
and setmeal.status = #{status}
</if>
</where>
</select>
特别强调!在SQL语句里,我们要查category.name的数据,没有问题,但如果直接这样写,而不加上as categoryName那就出大问题了。
至于为什么,我们可以去SetmealVO类里去看看(我们是用SetmealVO来封装查询结果的)会发现类里有一个属性
//分类名称
private String categoryName;
它是用来封装category.name的,所以我们如果不在SQL语句给category.name起一个别名categoryName的话,那么查出来的分类名称根据就无法封装到SetmealVO的类里,进而就无法传送到前端,然后前端的结果就会是下图的样子:
因此这里的SQL语句是一个坑,千万注意。
分页查询功能至此完毕。
为了完成删除套餐的功能,我们需要先完成启售、停售套餐的功能,因为根据业务需求,售卖中的套餐是不能直接删除的。
启售、停售套餐
启售停售套餐的功能比较简单,只是对数据库中status属性进行更新即可。因此这一模块作者不过多描述。
SetmealMapper.java
@PostMapping("/status/{status}")
@ApiOperation("启售、停售套餐")
public Result startOrStop(@PathVariable Integer status,Long id) {
log.info("启售、停售套餐:{},{}",status,id);
setmealService.startOrStop(status,id);
return Result.success();
}
接口文档作者就不提供了,大家自己分析即可,主要就是明确几点:请求路径、请求参数、请求方式、返回数据。
SetmealServiceMapper.java
//启售、停售套餐
void startOrStop(Integer status, Long id);
同理,我们去实现类里重写该方法
SetmealServiceImpl.java
@Override
public void startOrStop(Integer status, Long id) {
Setmeal setmeal = Setmeal.builder()
.status(status)
.id(id)
.build();
setmealMapper.update(setmeal);
}
这里我们沿用视频里的风格,运用了建造者设计模式。将前端传来的id和status赋值给setmeal对象。然后调用持久层方法即可。
SetmealMapper.java
@AutoFill(value = OperationType.UPDATE)
void update(Setmeal setmeal);
在这里我们写一个通用的更新操作SQL语句,所以我们在xml文件里编写
SetmealMapper.xml
<update id="update">
update setmeal
<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>
这样启售、停售的功能开发完成了。我们就可以着手完成删除套餐的功能了。
批量删除套餐
SetmealController.java
@DeleteMapping
@ApiOperation("批量删除套餐")
public Result delete(@RequestParam List<Long> ids) {
log.info("根据id批量删除套餐:{}",ids);
setmealService.deleteBtach(ids);
return Result.success();
}
从接口文档可知,请求方式是DELETE方式,前端传来的参数id是有多个的,所以我们用List集合来接收参数,别忘了加上注解@RequestParam。然后直接调用服务层方法即可。
SetmealService.java
//批量删除套餐
void deleteBtach(List<Long> ids);
然后去实现类里重写该方法
SetmealServiceImpl.java
@Override
@Transactional
public void deleteBtach(List<Long> ids) {
//判断当前套餐是否在启售中,启售中不能删除
for (Long id : ids) {
Setmeal setmeal = setmealMapper.getById(id);
if (setmeal.getStatus() == StatusConstant.ENABLE){
throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
}
}
//删除套餐表里的套餐
setmealMapper.deleteByIds(ids);
//删除套餐菜品表里的菜品
setmealDishMapper.deleteByDishIds(ids);
}
在处理业务的时候要主要业务逻辑,删除之前我们应该先判断一下套餐是否在停售状态,如果在停售状态,那么可以被删除;反之则不能被删除。所以我们循环遍历ids集合,对每一个id对应的套餐进行status的判断。经过判断后就说明都可以被删除。同时我们要注意,根据业务场景,删除一个套餐,那么对应的菜品都应该被删除,毕竟套餐里的菜品是存在setmeal_dish表里的,如果套餐删除了,而菜品没有删除,那么setmeal_dish里的菜品就没有任何意义了。
所以我们分两步进行,第一步删除套餐,第二步删除对应的菜品。
先看看删除套餐
SetmealMapper.java
void deleteByIds(List<Long> ids);
这里涉及到动态SQL语句,因此我们在xml文件里编写
<delete id="deleteByIds">
delete from setmeal where id in
<foreach collection="ids" separator="," item="id" open="(" close=")">
#{id}
</foreach>
</delete>
第一步完成了。接下来看删除对应的菜品
SetmealDishMapper.java
//根据套餐id批量删除
void deleteByDishIds(List<Long> ids);
同样,我们在xml文件里编写SQL语句
<delete id="deleteByDishIds">
delete from setmeal_dish where setmeal_id in
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</delete>
至此删除套餐功能开发完毕
根据ID查询套餐
修改套餐可能会稍微麻烦一些,因为我们还要再另外实现前端页面回显的功能,也就是说我们还要实现一个根据套餐id查询的功能。我们先完成回显功能。
@GetMapping("/{id}")
@ApiOperation("根据ID查询套餐")
public Result<SetmealVO> getBySetmealId(@PathVariable Long id) {
log.info("根据id查询套餐:{}",id);
SetmealVO setmealVOs = setmealService.getById(id);
return Result.success(setmealVOs);
}
接口文档作者就不过多分析了。需要注意前端传来的是路径参数,因此要用@PathVariable注解。
直接调用服务层即可。需要注意我们还要用一个SetmealVO对象来封装查询的结果,然后返回给前端。
SetmealService.java
//根据id查询套餐
SetmealVO getById(Long id);
SetmealServiceImpl.java
@Override
public SetmealVO getById(Long id) {
//根据id查询套餐
Setmeal setmeal = setmealMapper.getById(id);
//根据id查询对应菜品
List<SetmealDish> setmealDishs = setmealDishMapper.getBySetmealId(id);
//将结果封装到VO
SetmealVO setmealVO = new SetmealVO();
BeanUtils.copyProperties(setmeal,setmealVO);
setmealVO.setSetmealDishes(setmealDishs);
return setmealVO;
}
在这里的业务逻辑是,查询套餐,不仅是查询套餐本身的信息,还要查询套餐对应的菜品,所以再业务层这里,我们要分成两步来进行。分别查到套餐和菜品后,我们将结果统一封装到SetmealVO对象里。先用BeanUtils.copyProperties()方法将查到的setmeal里的属性拷贝给setmealVO,此时setmealVO还需要菜品属性的值,所以我们就调用setSetmealDishes方法赋值。
分别来看两个查询操作
SetmealMapper.java
@Select("select * from setmeal where id = #{id};")
Setmeal getById(Long id);
因为根据id查询套餐的SQL语句比较简单,所以我们直接用注解的方式来完成
接下来看查询菜品
SetmealDishMapper.java
@Select("select * from setmeal_dish where setmeal_id = #{id}")
List<SetmealDish> getBySetmealId(Long id);
同理,我们用注解的方式来完成。
至此根据ID查询套餐的功能完成了,接下来我们就可以实现修改套餐的功能了。
修改套餐
SetmealController.java
@PutMapping
@ApiOperation("修改套餐")
public Result update(@RequestBody SetmealDTO setmealDTO){
log.info("修改套餐:{}",setmealDTO);
setmealService.update(setmealDTO);
return Result.success();
}
控制层的代码作者就不描述了。
SetmealService.java
//修改套餐
void update(SetmealDTO setmealDTO);
SetmealServiceImpl.java
@Override
@Transactional
public void update(SetmealDTO setmealDTO) {
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO,setmeal);
//修改套餐
setmealMapper.update(setmeal);
List<Long> ids = new ArrayList<>(); //存放套餐id的集合,这样可以不用新定义一个根据单个id查询的方法
ids.add(setmealDTO.getId());
//删除原有的菜品
setmealDishMapper.deleteByDishIds(ids);
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
//插入套餐对应的菜品
if (setmealDishes != null && setmealDishes.size() > 0){
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealDTO.getId());
});
}
//批量插入菜品
setmealDishMapper.insertBatch(setmealDishes);
}
修改套餐的业务逻辑稍微复杂一些,但和视频里之前对菜品进行修改的逻辑是一样的,所以我们直接照用即可。作者在这里有个地方用到的方法稍微有点不一样
List<Long> ids = new ArrayList<>(); //存放套餐id的集合,这样可以不用新定义一个根据单个id查询的方法
ids.add(setmealDTO.getId());
//删除原有的菜品
setmealDishMapper.deleteByDishIds(ids);
由于我们修改菜品是先将对应的菜品全部删除再插入修改后的菜品。所以我们要调用持久层的删除操作,在上面我们开发删除套餐功能的时候写过一个删除对应菜品的方法,但那个方法需要接收的是封装了套餐id的集合,而在此处我们只是一个Long类型的id参数,因此我们创建一个List集合,将这单个的id存进List集合去,这样我们就可以直接使用我们之前写的删除方法,从而避免了重新再去写一个根据单个id删除菜品的方法。
要注意我们要给每一个菜品的setmealId赋值
if (setmealDishes != null && setmealDishes.size() > 0){
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealDTO.getId());
});
}
然后我们直接调用持久层的方法来批量插入菜品数据。
SetmealDishMapper.java
void insertBatch(List<SetmealDish> setmealDishes);
同样我们在xml文件里编写
SetmealDishMapper.xml
<insert id="insertBatch">
insert into setmeal_dish(setmeal_id, dish_id, name, price, copies) values
<foreach collection="setmealDishes" item="setmealDish" separator=",">
(#{setmealDish.setmealId},#{setmealDish.dishId},#{setmealDish.name},#{setmealDish.price},#{setmealDish.copies})
</foreach>
</insert>
至此,修改套餐功能开发完毕。