菜品管理
主要功能模块:新建菜品、修改菜品、启用禁用菜品、菜品的分页查询、删除菜品
代码:GitHub - Echo0701/take-out
1 公共字段自动填充
公共字段指的是业务表中有一些相同的字段,比如创建人、创建时间、修改人、修改时间等,我们在维护这些数据的时候,都需要为这几个字段来赋值,为避免代码重复,引入公共字段自动填充的内容。
1.1 问题分析
业务表中存在公共字段,导致 Java 中出现冗余的代码,后期如果进行变更,不方便维护
1.2 实现思路
明确上述字段的操作时机
- 自定义注解 AutoFill ,用于标识需要进行公共字段自动填充的方法
- 自定义切面类 AutoFillAspect ,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
- 在 Mapper 的方法上加入 AutoFill 注解
【技术点】枚举(标识当前操作的类型)、注解、AOP、反射
1.3 代码开发
AutoFill.java
/**
* 自定义注解,用于表示某个方法需要进行公共字段填充处理
*/
@Target(ElementType.METHOD) //指定注解只能加在方法上面
@Retention(RetentionPolicy.RUNTIME) //指明注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在,且可以在运行时通过反射获取到。这样的注解可以用来在运行时进行一些特殊的操作,例如动态生成代码、动态代理等
public @interface AutoFill {
//指定当前数据库的操作的类型:update insert
OperationType value();
}
AutoFillAspect.java
@Aspect
@Component
@Slf4j //记录日志
public class AutoFillAspect {
//定义切入点:哪些类的哪些方法来进行拦截
/**
* 切入点
*/
@Pointcut("execution(* com.sky.mapper.*.*(..) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut() {
}
/**
* 前置通知,在这个部分进行公共字段的赋值
*/
//定义前置通知,因为要在 update 和 insert 之前为公共字段赋值
//当匹配上切点表达式的时候就会执行我们这个通知的方法
@Before("autoFillPointCut")
public void autoFill(JoinPoint joinPoint) {
log.info("开始进行公共字段的填充...");
//1、获取到当前被拦截的方法上的数据库操作类型
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); //方法签名对象
AutoFill autoFill= signature.getMethod().getAnnotation(AutoFill.class); //获得方法上的注解对象
OperationType operationType = autoFill.value(); // 获得数据库操作类型
//2、获取到当前被拦截的方法的参数--实体对象
Object[] args= joinPoint.getArgs();
if(args == null || args.length == 0) {
return;
}
Object entity = args[0];
//3、准备赋值的数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
//4、根据当前的不同操作类型,为对应的属性通过反射来赋值
if (operationType == OperationType.INSERT) {
//为四个公共字段赋值
try {
//获得了四个公共字段的set方法
// Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime",LocalDateTime.class); 为了避免方法拼写错误,所以统一用方法常量,定义在common包
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME,LocalDateTime.class);
Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER,Long.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME,LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER,Long.class);
//通过反射来为对象属性赋值
setCreateTime.invoke(entity,now);
setCreateUser.invoke(entity,currentId);
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
throw new RuntimeException(e);
}
} else if (operationType == OperationType.UPDATE) {
//为两个公共字段赋值
try {
//获得了2个公共字段的set方法
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME,LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER,Long.class);
//通过反射来为对象属性赋值
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
2 新增菜品
2.1 需求分析和设计
产品原型
业务规则
- 菜品名称必须唯一
- 菜品必须属于某个分类下,不能单独存在
- 新增菜品时可以根据情况选择菜品的口味
- 每个菜品必须对应一张图片
接口设计
① 根据类型查询分类
② 文件上传
③ 新增菜品
数据库设计
【逻辑外键】:数据库里并没有将这个外界关系真的给创建出来,而是通过我们的程序自己去维护这个字段,换句话说,数据库并不认为当前这个字段是一个外界,但在程序中会将其当作外键字段来处理
2.2 代码开发
2.2.1 文件上传
利用阿里云OSS来存储对象,完成文件上传
OssConfiguration.java
package com.sky.config;
import com.sky.properties.AliOssProperties;
import com.sky.utils.AliOssUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 配置类,用于创建 AliOssUtil 对象
*/
@Configuration
@Slf4j
public class OssConfiguration {
@Bean
@ConditionalOnMissingBean //保证整个spring容器里只有一个util对象,当没有util的条件下才会创建这个bean
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) {
log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}
application-dev.yml
sky:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
host: localhost
port: 3306
database: sky_take_out
username: root
password: root
alioss:
endpoint: https://oss-cn-hangzhou.aliyuncs.com
access-key-id: LTAI5tLmKyefze8CGYs47HQt
access-key-secret: MvO3k0AIAm4GSNujs0fLug4Chyu4sB
bucket-name: cq-takeout-test
application.yml
sky:
jwt:
# 设置jwt签名加密时使用的秘钥
admin-secret-key: itcast
# 设置jwt过期时间
admin-ttl: 7200000
# 设置前端传递过来的令牌名称
admin-token-name: token
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}
【代码逻辑】配置文件里配置了几个配置项,这几个配置项通过配置属性类来加载,然后又编写了配置类,通过这个配置类就可以创建出所需要的对象
上传文件的通用控制类 CommonController.java
/**
* 通用接口
*/
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {
//这里的 file 要和前端接口请求的参数名相同
private AliOssUtil aliOssUtil;
/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
@ApiOperation("文件上传")
public Result<String> upload(MultipartFile file) {
log.info("文件上传:{}",file);
try {
//原始文件名
String originalFilename = file.getOriginalFilename();
//截取原始文件后缀名
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.info("文件上传失败:{}", e);
}
return Result.error(MessageConstant.UPLOAD_FAILED);
}
}
2.2.2 新增菜品
DishController.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();
}
}
DishServiceImpl.java
@Service
@Slf4j
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
private DishFlavorMapper dishFlavorMapper;
/**
* 新增菜品和口味
* @param dishDTO
*/
@Transactional
public void saveWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO,dish);
//向菜品表插入一条数据
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);
}
}
}
DishFlavorMapper.java
@Mapper
public interface DishFlavorMapper {
/**
* 批量插入口味数据
* @param flavors
*/
void insertBatch(List<DishFlavor> flavors);
//因为要用到动态sql,所以写到映射文件里面去
}
DishMapper.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);
//这里插入的数据很多,所以写到映射文件里面去
}
3 菜品分页查询
3.1 需求分析和设计
产品原型
业务规则
- 根据页码展示菜品信息
- 每页展示十条数据
- 分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询
接口设计
3.2 代码开发
① 根据菜品分页查询接口定义设计对应的DTO:
② 根据菜品分页查询接口定义设计对应的VO:
【注】把这个vo转换成json数据传到前端,前端就可以正常去显示
DishController.java
@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
log.info("菜品分页查询:{}",dishPageQueryDTO);
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}
DishServiceImpl.java
/**
* 菜品分页查询
* @param dishPageQueryDTO
* @return
*/
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
//利用PageHelper插件来进行分页操作
//开始分页
PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());
//开始查询
Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
return new PageResult(page.getTotal(),page.getResult());
}
4 删除菜品
4.1 需求分析和设计
产品原型
业务规则
- 可以一次删除一个菜品,也可以批量删除菜品
- 起售中的菜品不能删除
- 被套餐关联的菜品不能删除
- 删除菜品以后,关联的口味数据也需要删除掉
接口设计
数据库设计
【注】setmeal_dish表:套餐和菜品关系表
4.2 代码开发
DishServiceImpl.java
/**
* 菜品批量删除
* @param ids
*/
//加上事务注解,保证事务的一致性
@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);
}
}
DishController.java
/**
* 菜品批量删除
* @return
*/
@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids) {
log.info("菜品批量删除:{}",ids);
dishService.deleteBatch(ids);
return Result.success();
}
5 修改菜品
5.1 需求分析和设计
产品原型
接口设计
- 根据 id 查询菜品
- 根据类型查询分类(已实现)
- 文件上传(已实现)
- 修改菜品
① 根据 id 查询菜品
② 修改菜品
5.2 代码开发
DishController.java
/**
* 根据 id 查询菜品
* @param id
* @return
*/
@GetMapping("/{id}")
@ApiOperation("根据 id 查询菜品")
public Result<DishVO> getById(@PathVariable Long id) {
log.info("根据id查询菜品:{}",id);
DishVO dishVO = dishService.getByIdWithFlavor(id);
return Result.success(dishVO);
}
/**
* 修改菜品
* @param dishDTO
* @return
*/
@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishDTO) {
log.info("修改菜品:{}",dishDTO);
dishService.updateWithFlavor(dishDTO);
return Result.success();
}
DishServiceImpl.java
/**
* 根据 id 查询菜品和对应的口味数据
* @param id
* @return
*/
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;
}
/**
* 根据 id 修改菜品的基本信息和对应的口味信息
* @param dishDTO
*/
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);
}
}