课程内容

  • 公共字段自动填充
  • 新增菜品
  • 菜品分页查询
  • 删除菜品
  • 修改菜品

公共字段自动填充

问题分析

业务表中的公共字段

序号 字段名 含义 数据类型
1 create_time 创建时间 datetime
2 create_user 创建人id bigint
3 update_time 修改时间 datetime
4 update_user 修改人id bigint
而目前 在我们的项目中处理字段都在每一个业务方法下逐个进行赋值操作
代码冗余 不便于后期维护

实现思路

使用aop切面编程思想 完成公共字段填充功能

序号 字段名 含义 数据类型 操作类型
1 create_time 创建时间 datetime insert
2 create_user 创建人id bigint insert
3 update_time 修改时间 datetime insert、update
4 update_user 修改人id bigint insert、update
  • 定义注解AutoFill 用于标识需要进行公共字段自动填充的方法
  • 自定义切面类AutoAspect 统一拦截加入AutoFill注解的方法
  • 通过反射为公共字段赋值
  • 在mapper方法上加入AutoFill注解

代码开发

  • 根据如上步骤开发
  • 进入到sky-server模块,创建com.sky.annotation包。
//自定义注解 , 用于标识某个方法需要进行功能字段填充处理  
@Target(ElementType.METHOD)  
@Retention(RetentionPolicy.RUNTIME)  
public @interface AutoFill {  
    //指定当时的操作类型  
    OperationType value();  
}
  • 创建切面类
  • 在sky-server模块,创建com.sky.aspect包。
//自定义切面  
@Aspect  
@Component  
@Slf4j  
public class AutoFillAspect {  
    //指定切入点  
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.anntotation.AutoFill)")  
    public void autoFillPointCut() {  
  
    }  
  
    //使用前置通知  
    @Before("autoFillPointCut()")  
    public void autoFill(JoinPoint joinPoint) {  
        //获取当前被拦截到的方法上的数据类型  
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象  
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象  
        OperationType operationType = autoFill.value();//获得数据库操作类型  
  
        //获取到当前来街道的方法参数 实体对象  
        Object[] args = joinPoint.getArgs();  
        if (args == null || args.length == 0) {  
            log.warn("没有传入参数,无法进行自动填充");  
            return;  
        }  
        Object entity = args[0]; //假设第一个参数是实体对象   
        //准备赋值数据  
        LocalDateTime now = LocalDateTime.now(); //当前时间  
        Long currentId = BaseContext.getCurrentId(); //当前登录用户id  
  
        //根据同操作类型 使用反射赋值  
        if (operationType == OperationType.INSERT) {  
            //插入操作 为四个公共字段赋值  
            try {  
                Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime", LocalDateTime.class);  
                Method setCreateUser = entity.getClass().getDeclaredMethod("setCreateUser", Long.class);  
                Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);  
                Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);  
                  
                //通过反射为对象赋值  
                setCreateTime.invoke(entity, now); //设置创建时间  
                setUpdateUser.invoke(entity, now); //设置更新时间  
                setUpdateUser.invoke(entity, currentId); //设置创建用户id  
                setCreateUser.invoke(entity, currentId); //设置更新用户id  
            } catch (Exception e) {  
                throw new RuntimeException(e);  
            }  
        }else if (operationType == OperationType.UPDATE) {  
            //更新操作 为两个公共字段赋值  
            try {  
                Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);  
                Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);  
                setUpdateTime.invoke(entity, now); //设置更新时间  
                setUpdateUser.invoke(entity, currentId); //设置更新用户id  
            } catch (Exception e) {  
                throw new RuntimeException(e);  
            }  
        }  
  
    }  
}
  • 在需要拦截的mapper上添加注解
  • server层中对应的赋值代码可以删除

新增菜品

需求分析

业务规则

  • 菜品名称必须唯一
  • 菜品必须属于某个分类下 不能单独存在
  • 新增彩品时根据情况选择菜品的口味
  • 每个菜品对应一张图片

接口设计

  • 根据类型查询分类(已完成)
  • 文件上传
  • 新增菜品

表设计

设计菜品表和菜品口味表

字段名 数据类型 说明 备注
id bigint 主键 自增
name varchar(32) 菜品名称 唯一
category_id bigint 分类id 逻辑外键
price decimal(10,2) 菜品价格
image varchar(255) 图片路径
description varchar(255) 菜品描述
status int 售卖状态 1起售 0停售
create_time datetime 创建时间
update_time datetime 最后修改时间
create_user bigint 创建人id
update_user bigint 最后修改人id
字段名 数据类型 说明 备注
id bigint 主键 自增
dish_id bigint 菜品id 逻辑外键
name varchar(32) 口味名称
value varchar(255) 口味值

代码开发

文件上传实现

  • 采用阿里云的oss服务进行文件存储
  • 在sky-server模块 application-dev.yml中配置
sky:
  alioss:
    endpoint: oss-cn-hangzhou.aliyuncs.com
    access-key-id: LTAI5tPeFLzsPPT8gG3LPW64
    access-key-secret: U6k1brOZ8gaOIXv3nXbulGTUzy6Pd7//记得换成自己的
    bucket-name: sky-take-out
  • application.yml中
spring:
  profiles:
    active: dev    #设置环境
sky:
  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}

  • 读取oss配置(源代码已提供)
  • 在server模块中生成oss工具类对象
/**
 * 配置类,用于创建AliOssUtil对象
 */
@Configuration
@Slf4j
public class OssConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
        log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
        return new AliOssUtil(aliOssProperties.getEndpoint(),
                aliOssProperties.getAccessKeyId(),
                aliOssProperties.getAccessKeySecret(),
                aliOssProperties.getBucketName());
    }
}
  • 在server层中定义接口
//通用接口  
@Slf4j  
@RestController  
@RequestMapping("/admin/common")
@ApiOperation("通用接口")  
public class CommonController {  
    @Autowired  
    private AliOssUtil aliOssUtil;  
  
    @PostMapping("/upload")  
    @ApiOperation("文件上传")  
    public Result<String> upload(MultipartFile file) {  
        try {  
            //原始文件名  
            String originalFilename = file.getOriginalFilename();  
            //截取原始文件名的后缀  
            String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));  
            //用uuid防止重名  
            String object = UUID.randomUUID().toString() + suffix;  
            String filePath = aliOssUtil.upload(file.getBytes() , object);  
              
            return Result.success(filePath);  
        } catch (IOException e) {  
            log.error("文件上传失败", e);  
            throw new RuntimeException(e);  
        }  
  
    }  
}

新增菜品实现

  • 设计dto类
  • 在pojo模块中
@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<>();
}
  • Controller层
  • 创建DishController
/**
 * 菜品管理
 */
@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();
    }
}
  • server层接口以及实现
package com.sky.service;

import com.sky.dto.DishDTO;
import com.sky.entity.Dish;

public interface DishService {

    /**
     * 新增菜品和对应的口味
     *
     * @param dishDTO
     */
    public void saveWithFlavor(DishDTO dishDTO);

}

package com.sky.service.impl;


@Service
@Slf4j
public class DishServiceImpl implements DishService {

    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private DishFlavorMapper dishFlavorMapper;

    /**
     * 新增菜品和对应的口味
     *
     * @param dishDTO
     */
    @Transactional
    public void saveWithFlavor(DishDTO dishDTO) {

        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO, dish);

        //向菜品表插入1条数据
        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);//后绪步骤实现
        }
    }

}
  • Mapper层
  • DishMapper中添加
	/**
     * 插入菜品数据
     *
     * @param dish
     */
    @AutoFill(value = OperationType.INSERT)
    void insert(Dish dish);
  • 在/resources/mapper中创建DishMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<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>
  • 创建DishFlavorMapper.java
@Mapper
public interface DishFlavorMapper {
    /**
     * 批量插入口味数据
     * @param flavors
     */
    void insertBatch(List<DishFlavor> flavors);

}
  • 在/resources/mapper中创建DishFlavorMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<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>

菜品分页查询

需求分析

  • 根据页码展示菜品信息
  • 每页展示10条数据
  • 分页查询时可以根据需要输入菜品名称 菜品分类 菜品状态进行查询

代码开发

设计DTO类

  • 根据菜品分页查询接口定义设计对应的DTO
  • 在sky-pojo模块中
@Data
public class DishPageQueryDTO implements Serializable {

    private int page;
    private int pageSize;
    private String name;
    private Integer categoryId; //分类id
    private Integer status; //状态 0表示禁用 1表示启用

}

设计VO类

  • 在sky-pojo模块中
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DishVO 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 LocalDateTime updateTime;
    //分类名称
    private String categoryName;
    //菜品关联的口味
    private List<DishFlavor> flavors = new ArrayList<>();
}

Controller层

  • 根据接口定义创建DishController 的page分页查询方法
	/**
     * 菜品分页查询
     *
     * @param dishPageQueryDTO
     * @return
     */
    @GetMapping("/page")
    @ApiOperation("菜品分页查询")
    public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO) {
        log.info("菜品分页查询:{}", dishPageQueryDTO);
        PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);//后绪步骤定义
        return Result.success(pageResult);
    }

Service层接口

  • 在Service创建分页查询方法
PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);
  • 在Impl层实现
	@Override  
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {  
    PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());  
    Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);  
    long total = page.getTotal();  
    List<DishVO> records = page.getResult();  
    return new PageResult(total, records);  
}

Mapper层

  • 在DishMapper接口中声明PageQuery方法
Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);
  • 在DishMapper.xml中编写sql
<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>

菜品删除

需求分析

  • 可以一次删除一个菜品,也可以批量删除菜品
  • 起售中的菜品不能删除
  • 被套餐关联的菜品不能删除
  • 删除菜品后,关联的口味数据也需要删除掉

注意事项

  • 在dish表中删除菜品基本数据时,同时,也要把关联在dish_flavor表中的数据一块删除。
  • setmeal_dish表为菜品和套餐关联的中间表。
  • 若删除的菜品数据关联着某个套餐,此时,删除失败。
  • 若要删除套餐关联的菜品数据,先解除两者关联,再对菜品进行删除。

代码开发

Controller

  • 根据删除菜品的接口定义在DishController
/**
     * 菜品批量删除
     *
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("菜品批量删除")
    public Result delete(@RequestParam List<Long> ids) {
        log.info("菜品批量删除:{}", ids);
        dishService.deleteBatch(ids);//后绪步骤实现
        return Result.success();
    }

Service

  • 在DishService接口中声明deleteBatch方法
void deleteBatch(List<Long> ids);
  • 继续实现
  @Autowired
    private SetmealDishMapper setmealDishMapper;
	/**
     * 菜品批量删除
     *
     * @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);//后绪步骤实现
        }
    }

Mapper层

  • 在DishMapper层中声明getById方法
/**
     * 根据主键查询菜品
     *
     * @param id
     * @return
     */
    @Select("select * from dish where id = #{id}")
    Dish getById(Long id);
  • 创建SetmealDishMapper,声明getSetmealIdsByDishIds方法,并在xml文件中编写SQL:
package com.sky.mapper;

import com.sky.entity.SetmealDish;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper
public interface SetmealDishMapper {
    /**
     * 根据菜品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);
}

  • 编写xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<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>
  • 声明deleteById方法并配置SQL
/**
     * 根据主键删除菜品数据
     *
     * @param id
     */
    @Delete("delete from dish where id = #{id}")
    void deleteById(Long id);
  • 声明deleteByDishId方法并配置SQL
    /**
     * 根据菜品id删除对应的口味数据
     * @param dishId
     */
    @Delete("delete from dish_flavor where dish_id = #{dishId}")
    void deleteByDishId(Long dishId);

修改菜品

需求分析

根据id查询菜品

  • 根据类型查询分类(已实现)
  • 文件上传(已实现)
  • 修改菜品

代码开发

  • 根据id查询菜品实现

Controller

  • 在DishController中创建方法
    /**
     * 根据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);
    }

Service

  • 在DishService中声明方法
	/**
     * 根据id查询菜品和对应的口味数据
     *
     * @param id
     * @return
     */
    DishVO getByIdWithFlavor(Long id);
  • 记得实现
	/**
     * 根据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;
    }

Mapper

  • 在DishFlavorMapper中声明getByDishId方法,并配置SQL
    /**
     * 根据菜品id查询对应的口味数据
     * @param dishId
     * @return
     */
    @Select("select * from dish_flavor where dish_id = #{dishId}")
    List<DishFlavor> getByDishId(Long dishId);

  • 修改菜品实现

Controller

  • 根据修改菜品的接口定义在DishController中创建方法:
	/**
     * 修改菜品
     *
     * @param dishDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改菜品")
    public Result update(@RequestBody DishDTO dishDTO) {
        log.info("修改菜品:{}", dishDTO);
        dishService.updateWithFlavor(dishDTO);
        return Result.success();
    }
  • 在DishService接口中声明updateWithFlavor方法:
	/**
     * 根据id修改菜品基本信息和对应的口味信息
     *
     * @param dishDTO
     */
    void updateWithFlavor(DishDTO dishDTO);

Service

  • 在DishServiceImpl中实现updateWithFlavor方法:
	/**
     * 根据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);
        }
    }

Mapper

  • 在DishMapper中,声明update方法:
	/**
     * 根据id动态修改菜品数据
     *
     * @param dish
     */
    @AutoFill(value = OperationType.UPDATE)
    void update(Dish dish);
  • 并在DishMapper.xml文件中编写SQL:
<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>

想温柔的对待这个世界