主要功能模块:新建菜品、修改菜品、启用禁用菜品、菜品的分页查询、删除菜品
公共字段指的是业务表中有一些相同的字段,比如创建人、创建时间、修改人、修改时间等,我们在维护这些数据的时候,都需要为这几个字段来赋值,为避免代码重复,引入公共字段自动填充的内容。
业务表中存在公共字段,导致 Java 中出现冗余的代码,后期如果进行变更,不方便维护

明确上述字段的操作时机

【技术点】枚举(标识当前操作的类型)、注解、AOP、反射
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);
- }
- }
-
- }
- }
产品原型

业务规则
接口设计
① 根据类型查询分类

② 文件上传

③ 新增菜品

数据库设计

【逻辑外键】:数据库里并没有将这个外界关系真的给创建出来,而是通过我们的程序自己去维护这个字段,换句话说,数据库并不认为当前这个字段是一个外界,但在程序中会将其当作外键字段来处理
利用阿里云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);
- }
- }
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);
- //这里插入的数据很多,所以写到映射文件里面去
- }
产品原型

业务规则
接口设计

① 根据菜品分页查询接口定义设计对应的DTO:

② 根据菜品分页查询接口定义设计对应的VO:

【注】把这个vo转换成json数据传到前端,前端就可以正常去显示
DishController.java
- @GetMapping("/page")
- @ApiOperation("菜品分页查询")
- public Result
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());
- }
产品原型

业务规则
接口设计

数据库设计

【注】setmeal_dish表:套餐和菜品关系表
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
ids) { - log.info("菜品批量删除:{}",ids);
- dishService.deleteBatch(ids);
- return Result.success();
- }
产品原型

接口设计
① 根据 id 查询菜品

② 修改菜品

DishController.java
- /**
- * 根据 id 查询菜品
- * @param id
- * @return
- */
- @GetMapping("/{id}")
- @ApiOperation("根据 id 查询菜品")
- public Result
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);
- }
- }