黑马程序员Java项目实战《瑞吉外卖》,轻松掌握springboot + mybatis plus开发核心技术的真java实战项目——第四部分
- 1. 套餐管理
- 1.1 新增套餐
- 1.1.1 添加菜品数据回显
- 1.2 保存添加套餐
- 1.3 套餐信息分页查询
- 1.4 删除套餐
- 1.5 需要自己单独实现的功能
- 1.5.1 套餐管理的启售、停售
- 1.5.2 套餐管理的修改
- 1.5.3 后台订单展示和查询
1. 套餐管理
需求分析:
1.1 新增套餐
套餐就是菜品的集合。
后台系统中可以管理套餐信息,通过新增套餐功能来添加一个新的套餐,在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐。
数据模型:
新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal dish表插入套餐和菜品关联数据。所以在新增套餐时,涉及到两个表:
- setmeal——套餐表
- setmeal dish——套餐菜品关系表
代码开发:
准备工作:
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
- 实体类
SetmealDish
( 直接从课程资料中导入即可,Setmeal
实体前面课程中已经导入过了) - DTO
SetmealDto
(直接从课程资料中导入即可) Mapper
接口SetmealDishMapper
- 业务层接口
SetmealDishService
- 业务层实现类
SetmealDishServicelmpl
- 控制层
SetmealController
package com.itheima.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 套餐菜品关系
*/
@Data
public class SetmealDish implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//套餐id
private Long setmealId;
//菜品id
private Long dishId;
//菜品名称 (冗余字段)
private String name;
//菜品原价
private BigDecimal price;
//份数
private Integer copies;
//排序
private Integer sort;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
//是否删除
private Integer isDeleted;
}
创建mapper:
package com.itheima.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.SetmealDish;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SetmealDishMapper extends BaseMapper<SetmealDish> {
}
创建service:
package com.itheima.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.SetmealDish;
public interface SetmealDishService extends IService<SetmealDish> {
}
package com.itheima.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.SetmealDish;
import com.itheima.reggie.mapper.SetmealDishMapper;
import com.itheima.reggie.service.SetmealDishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class SetmealDishServiceImpl extends ServiceImpl<SetmealDishMapper, SetmealDish> implements SetmealDishService {
}
1.1.1 添加菜品数据回显
controller层代码:
在开发代码之前,需要梳理一下新增套餐时前端页面和服务端的交互过程:
- 页面
(backend/page/combo/add.html)
发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中 - 页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
- 页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
- 页面发送请求进行图片上传,请求服务端将图片保存到服务器
- 页面发送请求进行图片下载,将上传的图片进行回显
- 点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端
开发新增套餐功能,其实就是在服务端编写代码去处理前端页面发送的这6次请求即可。
第一个交互前面写了;分类管理通过type的值来控制在前端展示的是 菜品分类(type=1) 或者是 套餐分类(type=2)
第二个交互前面也写了,在categorycontroller
里面的list
方法;
第四和第五前面也写了;
第三个交互:前端请求的地址;
在DishController书写代码:
/**
* 根据条件查询对应的菜品数据
* @param dish
* @return
*/
@GetMapping("/list")
public R<List<Dish>> list(Dish dish){ //会自动映射的
//这里可以传categoryId,但是为了代码通用性更强,这里直接使用dish类来接受(因为dish里面是有categoryId的),以后传dish的其他属性这里也可以使用
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId() != null ,Dish::getCategoryId,dish.getCategoryId());
//添加条件,查询状态为1(起售状态)的菜品
queryWrapper.eq(Dish::getStatus,1);
//添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(queryWrapper);
return R.success(list);
}
控制台输出的sql语句:
SELECT id,name,category_id,price,code,image,description,status,sort,create_time,update_time,create_user,update_user,is_deleted FROM dish WHERE (category_id = ? AND status = ?) ORDER BY sort ASC,update_time DESC
1.2 保存添加套餐
实现要求:点击保存按钮,发送ajax请求,将套餐相关的数据以json形式提交到服务端;
前端提交请求:
前端携带的参数:重要
根据前端传过来的数据我们可以在后端确定我们需要在后端使用什么来接受前端的参数;
编写controller:上面的dishList,我们数据库并不需要这个数据,所以接收数据的实体类没有dishList这个属性也没有关系,前端传过来的数据都是自动映射到接收数据的实体类的属性上的,没有对应起来就不会映射。
涉及两张表的操作:套餐表和菜品表;
/**
* 新增套餐
* 涉及两张表的操作:套餐表和菜品表;
* @param setmealDto
* @return
*/
@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto){
setmealService.saveWithDish(setmealDto);
return R.success("新增套餐成功");
}
SetmealService中添加自定义的方法:
/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* @param setmealDto
*/
void saveWithDish(SetmealDto setmealDto);
@Autowired
SetmealDishService setmealDishService;
/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* @param setmealDto
*/
@Transactional
@Override
public void saveWithDish(SetmealDto setmealDto) {
//保存套餐的基本信息,操作setmeal,执行insert
this.save(setmealDto);
log.info(setmealDto.toString()); //查看一下这个套餐的基本信息是什么
//保存套餐和菜品的关联信息,操作setmeal_dish ,执行insert操作
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
//注意上面拿到的setmealDishes是没有setmeanlId这个的值的,通过debug可以发现
setmealDishes.stream().map((item)->{
item.setSetmealId(setmealDto.getId());
return item; //这里返回的就是集合的泛型
}).collect(Collectors.toList());
setmealDishService.saveBatch(setmealDishes); //批量保存
}
功能测试,自己测试;
1.3 套餐信息分页查询
需求分析:
代码开发:
前端发起的请求以及携带的参数:
查询分页:
在开发代码之前,需要梳理一下套餐 分页查询时前端页面和服务端的交互过程:
-
页面(backend/ page/combo/list.html)发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
-
页面发送请求,请求服务端进行图片下载,用于页面图片展示
开发套餐信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求
controller层代码编写:
/**
* 套餐分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name){
//分页构造器对象
Page<Setmeal> pageInfo = new Page<>(page,pageSize);
//构造条件查询对象
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据name进行like模糊查询
queryWrapper.like(name != null,Setmeal::getName,name);
//添加排序条件,根据更新时间降序排列
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo,queryWrapper);
/**
* 注意如果这里直接返回R.success(pageInfo),
* 虽然不会报错但是分页的数据的套餐分类的名字是显示不了的;
* 因为这个分页的泛型是Setmeal,Setmeal只封装了f分类的Id categoryId,没有分类的名称 name
* 所以又需要进行name的获取和设值
*/
return R.success(pageInfo);
}
bug修复:
/**
* 套餐分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name){
//分页构造器对象
Page<Setmeal> pageInfo = new Page<>(page,pageSize);
Page<SetmealDto> dtoPage = new Page<>(page,pageSize);
//构造条件查询对象
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据name进行like模糊查询
queryWrapper.like(name != null,Setmeal::getName,name);
//添加排序条件,根据更新时间降序排列
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo,queryWrapper);
//对象的拷贝 注意这里要把分页数据的全集合records给忽略掉
BeanUtils.copyProperties(pageInfo,dtoPage,"records");
List<Setmeal> records = pageInfo.getRecords();
//对records对象进行处理然后封装好赋值给list
List<SetmealDto> list = records.stream().map((item)->{
SetmealDto setmealDto = new SetmealDto();
//对setmealDto进行除categoryName的属性进行拷贝(因为item里面没有categoryName)
BeanUtils.copyProperties(item,setmealDto);
//获取分类id 通过分类id获取分类对象 然后再通过分类对象获取分类名
Long categoryId = item.getCategoryId();
//根据分类id获取分类对象 判断是否为null
Category category = categoryService.getById(categoryId);
if (category != null){
String categoryName = category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;
}).collect(Collectors.toList());
dtoPage.setRecords(list);
return R.success(dtoPage);
}
1.4 删除套餐
代码开发:
单个套餐删除前端发的请求和携带的参数:
套餐批量删除前端发的请求和携带的参数:
controller层开发
在SetmealService中添加自定义的方法:
/**
* 删除套餐,同时需要删除套餐和菜品的关联数据
* @param ids
*/
void removeWithDish(List<Long> ids);
实现该方法:
/**
* 删除套餐,同时需要删除套餐和菜品的关联数据
* @param ids
*/
@Override
@Transactional
public void removeWithDish(List<Long> ids) {
//sql语句应该是这样的:select count(*) setmeal where id in () and status = 1;
//查询套餐的状态,看是否可以删除
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper();
queryWrapper.in(Setmeal::getId,ids);
queryWrapper.eq(Setmeal::getStatus,1);
int count = this.count(queryWrapper);
//如果不能删除,抛出一个业务异常
if (count > 0){
throw new CustomException("套餐正在售卖中,不能删除");
}
//如果可以删除,先删除套餐表中的数据--setmeal
this.removeByIds(ids);
//删除关系表中的数据--setmeal_dish
//delete from setmeal_dish where setmeal_id in (1,2,3)
LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper();
lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);
setmealDishService.remove(lambdaQueryWrapper);
}
功能测试;
1.5 需要自己单独实现的功能
1.5.1 套餐管理的启售、停售
前端发来的请求:
根据前面菜品模块自己实现的功能,我们可以知道,我们只需要写一个批量处理的方法就可以完成单个或者是批量套餐的启售,停售;
SetmealController中的controller层代码:
/**
* 对菜品批量或者是单个 进行停售或者是起售
* @return
*/
@PostMapping("/status/{status}")
//这个参数这里一定记得加注解才能获取到参数,否则这里非常容易出问题
public R<String> status(@PathVariable("status") Integer status,@RequestParam List<Long> ids){
setmealService.updateSetmealStatusById(status,ids);
return R.success("售卖状态修改成功");
}
SetmealService中添加下面方法:
/**
* 根据套餐id修改售卖状态
* @param status
* @param ids
*/
void updateSetmealStatusById(Integer status,List<Long> ids);
该方法的实现:
/**
* 根据套餐id修改售卖状态
* @param status
* @param ids
*/
@Override
public void updateSetmealStatusById(Integer status, List<Long> ids) {
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper();
queryWrapper.in(ids !=null,Setmeal::getId,ids);
List<Setmeal> list = this.list(queryWrapper);
for (Setmeal setmeal : list) {
if (setmeal != null){
setmeal.setStatus(status);
this.updateById(setmeal);
}
}
}
1.5.2 套餐管理的修改
分为两步:数据回显示,和提交修改数据到数据库
前端点击套餐修改,前端发过来的请求
SetmealController 中添加下面的代码:
/**
* 回显套餐数据:根据套餐id查询套餐
* @return
*/
@GetMapping("/{id}")
public R<SetmealDto> getData(@PathVariable Long id){
SetmealDto setmealDto = setmealService.getDate(id);
return R.success(setmealDto);
}
SetmealService添加下面的代码:
/**
* 回显套餐数据:根据套餐id查询套餐
* @return
*/
SetmealDto getDate(Long id);
该方法的实现:
/**
* 回显套餐数据:根据套餐id查询套餐
* @return
*/
@Override
public SetmealDto getDate(Long id) {
Setmeal setmeal = this.getById(id);
SetmealDto setmealDto = new SetmealDto();
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper();
//在关联表中查询,setmealdish
queryWrapper.eq(id!=null,SetmealDish::getSetmealId,id);
if (setmeal != null){
BeanUtils.copyProperties(setmeal,setmealDto);
List<SetmealDish> list = setmealDishService.list(queryWrapper);
setmealDto.setSetmealDishes(list);
return setmealDto;
}
return null;
}
测试:数据回显成功
但是这样我们再点击添加菜品会发现,右边只展示菜品的价格并没有展示菜品对应的名称:
已选菜品中的菜品并没有展示对应的菜品名;
修改后的运行情况展示:
搜索框的作用:
把DishController.java
修改
@GetMapping("/list")
public R<List<Dish>> list(Dish dish)
{
if(dish.getId()!=null)
log.info(dish.getCategoryId().toString());
if(dish.getName()!=null)
log.info(dish.getName().toString());
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());
queryWrapper.like(dish.getName()!=null,Dish::getName,dish.getName());
queryWrapper.eq(Dish::getStatus,1);
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(queryWrapper);
return R.success(list);
}
controller层代码:
为了不把问题复杂化,我是先把相关的setmealDish内容移除然后再重新添加,这样就可以不用考虑dish重复的问题和哪些修改哪些没修改;
@PutMapping
public R<String> edit(@RequestBody SetmealDto setmealDto){
if (setmealDto==null){
return R.error("请求异常");
}
if (setmealDto.getSetmealDishes()==null){
return R.error("套餐没有菜品,请添加套餐");
}
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
Long setmealId = setmealDto.getId();
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SetmealDish::getSetmealId,setmealId);
setmealDishService.remove(queryWrapper);
//为setmeal_dish表填充相关的属性
for (SetmealDish setmealDish : setmealDishes) {
setmealDish.setSetmealId(setmealId);
}
//批量把setmealDish保存到setmeal_dish表
setmealDishService.saveBatch(setmealDishes);
setmealService.updateById(setmealDto);
return R.success("套餐修改成功");
}
另一套
@Override
public void updateWithDish(SetmealDto setmealDto) {
this.updateById(setmealDto);
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SetmealDish::getSetmealId,setmealDto.getId());
setmealDishService.remove(queryWrapper);
List<SetmealDish> list = setmealDto.getSetmealDishes();
list.stream().map((item)->{
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
setmealDishService.saveBatch(list);
}
1.5.3 后台订单展示和查询
点击订单明细,前端会发下面的请求:携带的数据是分页使查询用的;
先写个controller看能不能接收到前端传过来的参数:发现只要参数和前端传过来的参数名对应就可以拿到参数的
orders.java
package com.itheima.reggie.entity;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单
*/
@Data
public class Orders implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//订单号
private String number;
//订单状态 1待付款,2待派送,3已派送,4已完成,5已取消
private Integer status;
//下单用户id
private Long userId;
//地址id
private Long addressBookId;
//下单时间
private LocalDateTime orderTime;
//结账时间
private LocalDateTime checkoutTime;
//支付方式 1微信,2支付宝
private Integer payMethod;
//实收金额
private BigDecimal amount;
//备注
private String remark;
//用户名
private String userName;
//手机号
private String phone;
//地址
private String address;
//收货人
private String consignee;
}
OrdersMapper
package com.itheima.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.Orders;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OrdersMapper extends BaseMapper<Orders> {
}
OrdersService
package com.itheima.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.Orders;
public interface OrdersService extends IService<Orders> {
}
OrdersServiceImpl
package com.itheima.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.Orders;
import com.itheima.reggie.mapper.OrdersMapper;
import com.itheima.reggie.service.OrdersService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
@Slf4j
public class OrdersServiceImpl extends ServiceImpl<OrdersMapper,Orders> implements OrdersService {
}
主要使用到mybatis-plus动态sql语句的生成:
这里我就直接把功能直接写在controller层了,看自己需求分层;(本人这里偷个懒)
/**
* 后台查询订单明细
* @param page
* @param pageSize
* @param number
* @param beginTime
* @param endTime
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String number,String beginTime,String endTime){
//分页构造器对象
Page<Orders> pageInfo = new Page<>(page,pageSize);
//构造条件查询对象
LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
//添加查询条件 动态sql 字符串使用StringUtils.isNotEmpty这个方法来判断
//这里使用了范围查询的动态SQL,这里是重点!!!
queryWrapper.like(number!=null,Orders::getNumber,number)
.gt(StringUtils.isNotEmpty(beginTime),Orders::getOrderTime,beginTime)
.lt(StringUtils.isNotEmpty(endTime),Orders::getOrderTime,endTime);
orderService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}