万字超详细苍穹外卖学习笔记2

三、day03学习

1、公共字段自动填充

公共字段即类似create_time、update_time、create_user、update_user在初始创建插入时,或者是在修改时都需要进行set设置,导致出现许多重复的冗余代码,当前模块的目的就是让这些公共字段进行自动的填充,而不是需要我们进行手动的set

1.1、自定义注解

通过使用自定义的注解,标识自动填充的方法,使这些方法会明确只有标了该自动注解的才需要自动填充

java 复制代码
package com.sky.annotation;

import com.sky.enumeration.OperationType;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 自定义注解,用于标识需要自动填充的方法
@Target(ElementType.METHOD)         // 该注解只能用于方法上
@Retention(RetentionPolicy.RUNTIME) // 该注解在运行时有效
public @interface AutoFill {
    // 对于 create_time、update_time、create_user、update_user 这些公共字段,只有在 insert 和 update 方法中才需要自动填充
    // 可使用一个枚举类来定义数据库的操作类型,即对数据库的哪些操作需要进行自动填充
    OperationType value();
}
1.2、自定义切面类
java 复制代码
package com.sky.aspect;

import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.time.LocalDateTime;

@Aspect
@Component
public class AutoFillAspect {
    // 创建切入点,指定需要自动填充的方法
    // 匹配 com.sky.mapper 包下的所有方法,且方法上有 AutoFill 注解
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointcut() {}

    // 使用前置通知,在方法执行前自动填充
    @Before("autoFillPointcut()")
    public void autoFill(JoinPoint joinPoint) {
        // 1、获取当前被拦截的方法上的数据库操作类型
        // 从连接点中获取方法签名
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // 获取该方法上的 AutoFill 注解
        AutoFill autoFill = methodSignature.getMethod().getAnnotation(AutoFill.class);
        // 获取 AutoFill 注解中的数据库操作类型
        OperationType operationType = autoFill.value();

        // 2、获取当前被拦截的方法的参数--即各个方法中的实体类
        // 由于不同方法的实体类不同,所以采用 Object 数组接收方法的所有参数(注意:我们约定实体类作为参数时,必须放在第一个参数)
        Object[] args = joinPoint.getArgs();
        if(args == null||args.length == 0){
            return;
        }

        // 3、准备要填充的数据,即要为 create_time、create_user、update_time、update_user 赋的值
        LocalDateTime now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();

        // 4、根据数据库操作类型,为对应的属性赋值
        if(operationType == OperationType.INSERT){
            // 为实体类中的 create_time、create_user、update_time、update_user 赋值
            // 由于我们约定实体类作为参数时,必须放在第一个参数,所以直接取第一个参数即可
            Object entity = args[0];
            try {
                // 获取要赋值的参数的 set 方法
                // 通过反射机制的 getMethod 方法获取要赋值的参数的 set 方法
                // public Method getMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException
                //  --name:要获取的方法的名称
                //  --parameterTypes:要获取的方法的参数类型列表
                Method setCreateTimeMethod = entity.getClass().getMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                Method setCreateUserMethod = entity.getClass().getMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                Method setUpdateTimeMethod = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUserMethod = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
                // 调用 set 方法,为属性赋值,此处使用了反射机制
                setCreateTimeMethod.invoke(entity, now);
                setCreateUserMethod.invoke(entity, currentId);
                setUpdateTimeMethod.invoke(entity, now);
                setUpdateUserMethod.invoke(entity, currentId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }else if(operationType == OperationType.UPDATE){
            // 为实体类中的 update_time、update_user 赋值
            // 由于我们约定实体类作为参数时,必须放在第一个参数,所以直接取第一个参数即可
            Object entity = args[0];
            try {
                // 获取要赋值的参数的 set 方法
                Method setUpdateTimeMethod = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUserMethod = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
                // 调用 set 方法,为属性赋值,此处使用了反射机制
                setUpdateTimeMethod.invoke(entity, now);
                setUpdateUserMethod.invoke(entity, currentId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
1.3、为mapper添加注释

注意只展示了添加了自定义注解的方法,其他方法暂时隐藏

java 复制代码
@Mapper
public interface CategoryMapper {
    // 修改分类,直接使用动态更新
    // 启用禁用分类,直接使用动态更新,将 status 字段更新为传入的 status 值
    @AutoFill(OperationType.UPDATE)
    void update(Category category);

    // 新增分类
    // 新增分类,由于只是单句 SQL 语句故使用注解而不是 xml 文件映射(已开启驼峰命名)
    // id 由于是设置了自增,故无需传入
    @Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
            " values(#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
    @AutoFill(OperationType.INSERT)
    void insert(Category category);
}
java 复制代码
@Mapper
public interface EmployeeMapper {
    // 新增员工,由于只是单句 SQL 语句故使用注解而不是 xml 文件映射(已开启驼峰命名)
    // id 由于是设置了自增,故无需传入
    @Insert("insert into employee(username, name, password, phone, sex, id_number, status, create_time, update_time, create_user, update_user)" +
            " values(#{username}, #{name}, #{password}, #{phone}, #{sex}, #{idNumber}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
    // 使用自定义注解 AutoFill 自动填充 create_time, update_time, create_user, update_user 字段
    // 数据库操作类型为 INSERT
    // 当切面类扫描到该方法时,会自动调用切面类中的通知方法
    @AutoFill(OperationType.INSERT)
    void insert(Employee employee);

    // 启用禁用员工账号,本质是根据 id 去数据库中修改员工的 status 属性
    // 编辑员工信息,本质是根据 id 去数据库中修改员工的其他属性
    // 使用自定义注解 AutoFill 自动填充 update_time, update_user 字段
    // 数据库操作类型为 UPDATE
    // 当切面类扫描到该方法时,会自动调用切面类中的通知方法
    @AutoFill(OperationType.UPDATE)
    void update(Employee employee); 
}
1.4、去除手动赋值公共字段

即将service的实现类中的手动赋值去掉

1.5、测试及结果

注意如果你在调试时,发现你的create_id或者update_id是null,可能是因为拦截器没有生效,建议重新去检查拦截器

2、新增菜品

根据接口文档,该业务涉及三个部分:根据类型查询分类、文件上传、新增菜品

  • 根据类型查询分类(已在day02中完成)
  • 文件上传可使用本地存储或者使用阿里云oss(云存储服务)(未完成)

以阿里云oss为例,文件上传后通过其返回的图片的url地址返回给前端,以达到当用户上传图片后可以直接通过前端的data的url地址查看到图片

  • 新增菜品(未完成)
2.1、文件上传
①、配置阿里云oss
Ⅰ、总配置文件
yaml 复制代码
sky:
	# 。。。。此处省略其他无关配置
  # 配置阿里云的 oss
  alioss:
    # 由于我已经在系统环境中配置了 access-key-id 和 access-key-secret,故这里不需要明文配置
    # access-key-id:
    # access-key-secret:

    # 当前是设置的是 dev 环境下的 yml,故需要在 application-dev.yml中进行以下的配置
    endpoint: ${sky.alioss.endpoint}
    bucket-name: ${sky.alioss.bucket-name}
Ⅱ、开发配置文件
yaml 复制代码
sky:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    host: localhost
    port: 3306
    database: sky_take_out
    username: root
    password: 123456
  alioss:
  # 注意要和你的阿里云 oss 的 bucket 名以及创建时设置的地域相同
  # 如果没有在系统环境中配置 access-key-id 和 access-key-secret,这里需要额外添加这两项并明文设置
    bucket-name: tbweb1
    endpoint: oss-cn-shenzhen.aliyuncs.com
②、初始化工具类
java 复制代码
@Component
// 该注解可以将配置文件中的属性绑定到 Bean 中
// 即在配置文件中的变量属性可映射到该类,与之对应
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {
//    原配置类
//    private String endpoint;
//    private String accessKeyId;
//    private String accessKeySecret;
//    private String bucketName;

//    由于我将 accesskey 的 id和 secret 配置在了系统的环境变量,所以此处为了可识别到采用环境变量注入的方法
    private String endpoint;
    @Value("${OSS_ACCESS_KEY_ID}")
    private String accessKeyId;
    @Value("${OSS_ACCESS_KEY_SECRET}")
    private String accessKeySecret;
    private String bucketName;

}

上面这一步是因为我并没有采用明文在配置文件中配置密钥的id和secret,所以需要采用系统环境变量注入的方法进行优化

java 复制代码
@Configuration
@Slf4j
// 该配置类是为了给 OssUtil 工具类中的成员变量进行初始化赋值
public class OssConfiguration {
    @Bean
    @ConditionalOnMissingBean   // 该注解是为了确保只有一个该工具类的 Bean 被创建
    // 通过 AliOssProperties(与配置文件相映射)赋值返回一个 AliOssUtil 的工具类
    public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) {
        // 通过 log 记录创建 AliOssUtil 工具类的 Bean 时的参数,确保参数的正确性
        log.info("开始创建 AliOssUtil 工具类的 Bean,参数为:{}", aliOssProperties);
        return new AliOssUtil(aliOssProperties.getEndpoint()
                ,aliOssProperties.getAccessKeyId()
                ,aliOssProperties.getAccessKeySecret()
                ,aliOssProperties.getBucketName());
    }
}
③、controller层测试
java 复制代码
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {

    @Autowired
    private AliOssUtil aliOssUtil;
    @PostMapping("/upload")
    // 通过接口文档可见,该方法的三项返回值中的 data(表示文件上传后的请求路径) 是 String 类型,且必须包含
    // 注意传进去的是 MultipartFile file,该变量名要和前端的请求参数名一致
    public Result<String> upload(MultipartFile file) {
        try {
            String originalFilename = file.getOriginalFilename();
            // 建议使用 UUID+文件后缀名的方式来命名上传到 oss 的文件,避免文件名冲突
            // 文件后缀名可通过对初始文件名的 "."截取获得
            // 例如:"image.jpg" 的后缀名就是 ".jpg"
            String objectName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));
            // upload 方法返回的是文件的请求路径
            String filePath = aliOssUtil.upload(file.getBytes(), objectName);
            return Result.success(filePath);
        } catch (IOException e) {
            log.error("文件上传失败:{}", e.getMessage());
        }
        // 通过定义好的常量返回上传失败的信息
        return Result.error(MessageConstant.UPLOAD_FAILED);
    }
}

通过前后端联调的方式进行测试(当然也可以通过 Postman进行测试)

2.2、新增菜品

由接口文档可见,该功能的请求参数较多,我们可以通过将这些多个参数封装为一个DishDTO,其中的flavors又通过创建一个 DishFlavor类进行封装

①、controller层
java 复制代码
@RestController
// 由于 nginx 的反向代理配置,已将初始路径设置为:http://localhost/api/ 或者也可以设置为 http://localhost:8080/
@RequestMapping("/dish")
@Slf4j
// 菜品相关接口
public class DishController {
    @Autowired
    private DishService dishService;
    @PostMapping
    // 新增菜品
    // 根据接口文档可见将请求参数封装为 DishDTO 对象,故需要使用 @RequestBody 注解
    // 请求参数默认是 @RequestParam(可写可不写)
    public Result save(@RequestBody DishDTO dishDTO) {
        log.info("新增菜品");
        dishService.saveWithFlavor(dishDTO);
        return Result.success();
    }
}
②、service层
java 复制代码
public interface DishService {
    // 新增菜品和口味
    void saveWithFlavor(DishDTO dishDTO);
}
java 复制代码
@Service
public class DishServiceImpl implements DishService {
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private DishFlavorMapper dishFlavorMapper;

    // 实现新增菜品和口味
    // 本方法将对菜品表和菜品口味表进行新增操作,为保持数据一致性,需要在一个事务中完成(数据原子性)
    @Transactional
    public void saveWithFlavor(DishDTO dishDTO){
        // 1、每次新增一条菜品数据
        // 由于我们目前只需要对菜品表进行操作,而不需要对菜品口味表进行操作,所以直接创建一个新的 Dish 实体类
        Dish dish = new Dish();
        // 从 DTO 中复制属性到实体类
        BeanUtils.copyProperties(dishDTO, dish);
        dishMapper.insert(dish);
        // 2、每次新增多条菜品口味数据
        // 从 DTO 中获取菜品口味列表
        List<DishFlavor> flavors = dishDTO.getFlavors();
        // 为每个口味设置菜品 id
        // 由于我们在 DishMapper.xml 中配置了主键自增,所以可以直接从实体类中获取 id
        flavors.forEach(flavor -> flavor.setDishId(dish.getId()));
        // 批量新增菜品口味数据
        dishFlavorMapper.insertBatch(flavors);
    }
}
③、mapper层
java 复制代码
@Mapper
public interface DishMapper {
    // 根据分类id查询菜品数量
    @Select("select count(id) from dish where category_id = #{categoryId}")
    Integer countByCategoryId(Long categoryId);

    // 新增菜品,使用自定义的注解自动填充公共字段
    @AutoFill(value = OperationType.INSERT)
    void insert(Dish dish);
}
java 复制代码
@Mapper
public interface DishFlavorMapper {
        // 新增菜品口味数据
        void insertBatch(List<DishFlavor> flavors);
}
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">
    <!--   新增菜品     -->
    <!-- 主键 id 自增,所以不需要手动设置 -->
    <!-- useGeneratedKeys="true"代表开启主键自增,keyProperty="id"代表将自增的主键值赋值给实体类的id属性 -->
    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into dish (name, category_id, price, image, description, status, create_time, update_time, create_user, update_user)
        values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})
    </insert>
</mapper>
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
--         取出集合 flavors 中的数据,使用 flavor变量进行接收,通过 foreach 循环批量插入,以逗号分隔
        <foreach collection="flavors" item="flavor" separator=",">
            (#{flavor.dishId}, #{flavor.name}, #{flavor.value})
        </foreach>
    </insert>

</mapper>
④、测试

3、菜品分页查询

由接口文档可见,该功能的请求参数较多,我们可以通过将这些多个参数封装为一个DishPageQueryDTO

同时,返回的data参数也较多,我们可以将data返回的多个参数封装为一个DishVO

请求参数复杂------>DTO

返回参数复杂------>VO(展示给前端的)

①、controller层
java 复制代码
@RestController
// 由于 nginx 的反向代理配置,已将初始路径设置为:http://localhost/api/ 或者也可以设置为 http://localhost:8080/
@RequestMapping("/dish")
@Slf4j
// 菜品相关接口
public class DishController {
    @Autowired
    private DishService dishService;

    @PostMapping
    // 新增菜品
    // 根据接口文档可见将请求参数(json格式)封装为 DishDTO 对象,故需要使用 @RequestBody 注解
    // 请求参数默认是 @RequestParam(可写可不写)
    public Result save(@RequestBody DishDTO dishDTO) {
        log.info("新增菜品");
        dishService.saveWithFlavor(dishDTO);
        return Result.success();
    }

    @GetMapping("/page")
    // 分页查询菜品
    // 根据接口文档可将请求参数封装为一个 DishPageQueryDTO 对象
    // 由于请求参数不是json格式,而是地址栏参数,所以不需要 @RequestBody 注解
    public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO) {
        log.info("分页查询菜品");
        PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
        return Result.success(pageResult);
    }
}
②、service层
java 复制代码
public interface DishService {
    // 分页查询菜品
    PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);
}
java 复制代码
@Service
public class DishServiceImpl implements DishService {
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private DishFlavorMapper dishFlavorMapper;

    // 实现分页查询菜品
    public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
        // 1、使用 PageHelper 工具类进行分页查询
        // 第一个参数为当前页码,第二个参数为每页显示记录数
        PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
        // 2、调用 mapper 持久层去进行数据库的查询
        // 此处使用 DishVO 而不是实体类和 DTO,是因为我们需要在分页查询结果中包含分类名称(这是接口文档中返回给前端的一个额外字段)
        // 注意必须返回的是 Page<DishVO> 类型,而不是 List<DishVO> 类型,这是使用 PageHelper 工具类的要求
        // 因为 PageHelper 工具类会自动将查询结果封装为 Page<DishVO> 类型
        Page<DishVO> page = dishMapper.mypageQuery(dishPageQueryDTO);
        // 3、将 page 分页查询结果包装为 PageResult 类型
        // 第一个参数为总记录数,第二个参数为当前页记录列表
        long total = page.getTotal();
        List<DishVO> dishList = page.getResult();
        // 4、返回 PageResult 类型的分页查询结果
        return new PageResult(total, dishList);
    }
}
③、mapper层
java 复制代码
@Mapper
public interface DishMapper {
    // 根据分类id查询菜品数量
    @Select("select count(id) from dish where category_id = #{categoryId}")
    Integer countByCategoryId(Long categoryId);

    // 新增菜品,使用自定义的注解自动填充公共字段
    @AutoFill(value = OperationType.INSERT)
    void insert(Dish dish);

    // 分页查询菜品
    Page<DishVO> mypageQuery(DishPageQueryDTO dishPageQueryDTO);
}
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">
    <!--   新增菜品     -->
    <!-- 主键 id 自增,所以不需要手动设置 -->
    <!-- useGeneratedKeys="true"代表开启主键自增,keyProperty="id"代表将自增的主键值赋值给实体类的id属性 -->
    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into dish (name, category_id, price, image, description, status, create_time, update_time, create_user, update_user)
        values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})
    </insert>

    <!-- 分页查询   -->
    <!-- 输入的参数是 DishPageQueryDTO,返回的结果是 DishVO,用于前端展示  -->
    <select id="mypageQuery" parameterType="com.sky.dto.DishPageQueryDTO" resultType="com.sky.vo.DishVO">
--      将 dish 表和 category 表进行左连接,将 category 表中的 name 命名为 categoryName 字段
        select d.*, c.name as categoryName
        from dish d
        left join category c on d.category_id = c.id
        <where>
--          当 name 不为空时,模糊查询含手动输入的 name 字段
            <if test="name != null and name != ''"> 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>

</mapper>
④、测试

由于我们的令牌在appropriate.yml中是有设置有效时长的,即超过设置的时间会过期,当在查询时报错401就是因为令牌过期,我们需要重新登录来获得新的有效的令牌

可见成功完成分页查询,同时如果是使用前后端联调也可以正常查看

4、删除菜品

由接口文档可见,该功能的请求参数较多,我们可以通过将这些多个参数封装为一个DishPageQueryDTO

同时,返回的data参数也较多,我们可以将data返回的多个参数封装为一个DishVO

①、controller层
java 复制代码
@RestController
// 由于 nginx 的反向代理配置,已将初始路径设置为:http://localhost/api/ 或者也可以设置为 http://localhost:8080/
@RequestMapping("/dish")
@Slf4j
// 菜品相关接口
public class DishController {
    @Autowired
    private DishService dishService;

    @DeleteMapping
    // 菜品批量删除
    public Result delete(@RequestParam List<Long> ids) {
        dishService.deleteBatch(ids);
        return Result.success();
    }
}
②、service层
java 复制代码
public interface DishService {
    // 菜品批量删除
    void deleteBatch(List<Long> ids);
}
java 复制代码
@Service
public class DishServiceImpl implements DishService {
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private DishFlavorMapper dishFlavorMapper;
    @Autowired
    private SetmealDishMapper setmealDishMapper;
  
    // 实现菜品批量删除
    // 本方法是对多个表一起进行操作,需要确保数据一致性,所以需要在一个事务中完成(数据原子性)
    // 如:删除菜品前需确保其是否包含在其他套餐中或是否在起售状态,否则会导致数据不一致;删除菜品后其对应的口味数据也要一起删除
    // 具体删除规则应见接口文档
    @Transactional
    public void deleteBatch(List<Long> ids) {
        // 1、判断是否在起售状态
        for(Long id : ids) {
            // 通过遍历获取到每个 id 对应的菜品实体类
            Dish dish = dishMapper.getById(id);
            if(dish.getStatus() == 1) {
                // 菜品在起售状态,不能删除
                // 抛出自定义的异常,同时通过定义的常量类传递异常信息
                throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
            }
        }
        // 2、判断是否和其他套餐关联
        // 通过传入的菜品 id 查询是否和其他套餐关联
        List<Long > setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
        if(setmealIds != null && setmealIds.size() > 0) {
            // 菜品关联了其他套餐,不能删除,抛出自定义的异常,同时通过定义的常量类传递异常信息
            throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
        }
        // 3、如果不在起售且不和套餐关联则删除菜品,同时删除其对应的口味数据
//        for(long id : ids){
//            dishMapper.deleteById(id);
//            dishFlavorMapper.deleteBatchByDishIds(id);
//        }
        // 原版本每次删除均要进行遍历并且分别删除菜品和口味数据,效率较低
        // 优化版本:直接批量删除菜品数据,同时通过关联关系删除其对应的口味数据
        dishMapper.deleteBatch(ids);
        dishFlavorMapper.deleteBatchByDishIds(ids);
    }
}
③、mapper层

包括对DishMapper、DishFlavorMapper、SetmealDishMapper的设计

java 复制代码
@Mapper
public interface DishMapper {
    // 批量删除菜品
//    @Delete("delete from dish where id in (#{id})")
//    void deleteById(Long id);
    // 优化版本
    void deleteBatch(List<Long> ids);
}
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">
       <!--    批量删除菜品     -->
    <delete id="deleteBatch">
        delete from dish where id in
        <foreach collection="ids" item="id" open="(" close=")" separator=",">
            #{id}
        </foreach>
    </delete>
</mapper>
java 复制代码
@Mapper
public interface DishFlavorMapper {

        // 根据菜品id删除菜品口味数据
//        @Delete("delete from dish_flavor where dish_id = #{dishId}")
//        void deleteBatchByDishIds(Long dishId);
        // 优化版本
        void deleteBatchByDishIds(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.DishFlavorMapper">
    <!--    根据菜品 id 批量删除菜品口味数据     -->
    <delete id="deleteBatchByDishIds">
        delete from dish_flavor where dish_id in
        <foreach collection="dishIds" item="dishId" open="(" close=")" separator=",">
            #{dishId}
        </foreach>
    </delete>

</mapper>
java 复制代码
@Mapper
public interface SetmealDishMapper {
    // 通过传入的菜品 id 查询是否和其他套餐关联
    List<Long> getSetmealIdsByDishIds(List<Long> ids);
}
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">
    <!-- 通过传入的菜品 id 查询是否和其他套餐关联 -->
    <!-- 通过遍历 List<Long> ids,将其中的每个元素称作 id,以逗号分隔,(开头,)结尾,作为 SQL 语句的参数 -->
    <select id="getSetmealIdsByDishIds" resultType="java.lang.Long">
        select setmeal_id from setmeal_dish where dish_id in
        <foreach collection="ids" item="id" open="(" close=")" separator=",">
            #{id}
        </foreach>
    </select>
</mapper>
④、测试

5、修改菜品

由接口文档可见,该功能包含:根据id查询菜品(未完成)、根据分类查询菜品(已完成)、文件上传(已完成)、修改菜品(未完成)

  • 根据id查询菜品:其返回的data应包含flavors集合------>故不是实体类Dish;还包含categoryName------>也不是DishDTO对象,与其返回的data数据相对应的是 DishVO对象
  • 修改菜品:
5.1、根据id查询菜品
①、controller层
java 复制代码
@RestController
// 由于 nginx 的反向代理配置,已将初始路径设置为:http://localhost/api/ 或者也可以设置为 http://localhost:8080/
@RequestMapping("/dish")
@Slf4j
// 菜品相关接口
public class DishController {
    @Autowired
    private DishService dishService;

    @GetMapping("/{id}")
    // 根据id查询菜品
    // 由接口文档可见路径参数是 id,所以需要使用 @PathVariable 注解
    // 参数名要与路径参数名一致,这样即可省略写成 @PathVariable Long id
    public Result<DishVO> getById(@PathVariable("id") Long id) {
        DishVO dishVO = dishService.getByIdWithFlavor(id);
        return Result.success(dishVO);
    }
}
②、service层
java 复制代码
public interface DishService {
    // 根据id查询菜品和口味
    DishVO getByIdWithFlavor(Long id);
}
java 复制代码
@Service
public class DishServiceImpl implements DishService {
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private DishFlavorMapper dishFlavorMapper;
    @Autowired
    private SetmealDishMapper setmealDishMapper;
  
    // 实现根据id查询菜品和口味
    // 本接口是对两个表进行查询操作,由于查询操作不涉及数据修改,所以不需要在一个事务中完成(数据一致性)
    public DishVO getByIdWithFlavor(Long id) {
        // 1、根据 id 查询菜品实体类
        Dish dish = dishMapper.getById(id);
        // 2、根据 id 查询菜品口味列表
        List<DishFlavor> flavors = dishFlavorMapper.getByDishId(id);
        // 3、将菜品实体类和口味列表封装为 DishVO 类型
        DishVO dishVO = new DishVO();
        BeanUtils.copyProperties(dish, dishVO);
        // Dish 实体类中只包含除 flavors集合、categoryName 外的所有属性
        // 所以需要手动设置 flavors 集合和 categoryName 属性,其中 categoryName 是从分类表中查询得到的,不需要额外设置
        dishVO.setFlavors(flavors);
        // 4、返回 DishVO 类型的查询结果
        return dishVO;
    }
}
③、mapper层
java 复制代码
@Mapper
public interface DishFlavorMapper {

        // 根据菜品id查询菜品口味数据
        @Select("select * from dish_flavor where dish_id = #{dishId}")
        List<DishFlavor> getByDishId(Long dishId);
}
④、测试
5.2、修改菜品
①、controller层
java 复制代码
@RestController
// 由于 nginx 的反向代理配置,已将初始路径设置为:http://localhost/api/ 或者也可以设置为 http://localhost:8080/
@RequestMapping("/dish")
@Slf4j
// 菜品相关接口
public class DishController {
    @Autowired
    private DishService dishService;

    @PutMapping
    // 更新菜品以及其对应口味
    // 根据接口文档可见将请求参数(json格式)封装为 DishDTO 对象,故需要使用 @RequestBody 注解
    // 请求参数默认是 @RequestParam(可写可不写)
    public Result update(@RequestBody DishDTO dishDTO) {
        dishService.updateWithFlavor(dishDTO);
        return Result.success();
    }
}
②、service层
java 复制代码
public interface DishService {
        // 更新菜品和口味
    void updateWithFlavor(DishDTO dishDTO);
}
java 复制代码
@Service
public class DishServiceImpl implements DishService {
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private DishFlavorMapper dishFlavorMapper;
    @Autowired
    private SetmealDishMapper setmealDishMapper;
  
    // 实现更新菜品和口味
    // 本方法是对两个表进行操作,需要确保数据一致性,所以需要在一个事务中完成(数据原子性)
    @Transactional
    public void updateWithFlavor(DishDTO dishDTO) {
        // 1、修改菜品实体类
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO, dish);
        dishMapper.update(dish);
        // 2、删除原有口味数据
        // 通过使用 Collections.singletonList() 方法将 id 封装为一个元素的列表,
        List<Long> dishIds = Collections.singletonList(dishDTO.getId());
        dishFlavorMapper.deleteBatchByDishIds(dishIds);
        // 3、批量新增新的口味数据
        List<DishFlavor> flavors = dishDTO.getFlavors();
        // 为每个口味数据设置菜品 id
        if(flavors != null && !flavors.isEmpty()) {
            // 由于我们在 DishMapper.xml 中配置了主键自增,所以可以直接从实体类中获取 id
            flavors.forEach(flavor -> flavor.setDishId(dish.getId()));
        }
        dishFlavorMapper.insertBatch(flavors);
    }
}
③、mapper层
java 复制代码
@Mapper
public interface DishMapper {
    // 更新菜品实体类
    @AutoFill(value = OperationType.UPDATE)
    void update(Dish dish);
}
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">
        <!-- 更新菜品实体类 -->
        <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>
                update_time=#{updateTime}, update_user=#{updateUser}
            </set>
            where id=#{id}
        </update>
</mapper>
④、测试
相关推荐
Nandeska2 小时前
13、MySQL半同步复制示例
数据库·mysql
短剑重铸之日2 小时前
《设计模式》第六篇:装饰器模式
java·后端·设计模式·装饰器模式
液态不合群2 小时前
【面试题】MySQL 中 count(*)、count(1) 和 count(字段名) 有什么区别?
android·数据库·mysql
像少年啦飞驰点、2 小时前
零基础入门 Spring Boot:从‘Hello World’到可上线微服务的完整学习路径
java·spring boot·web开发·编程入门·后端开发
心 -2 小时前
全栈实时聊天室(java项目)
java
ytgytg282 小时前
HC小区管理系统安装,提示redis连接错误
数据库·redis·缓存
1104.北光c°2 小时前
【从零开始学Redis | 第一篇】Redis常用数据结构与基础
java·开发语言·spring boot·redis·笔记·spring·nosql
怣502 小时前
MySQL聚合函数在查询中的五大核心应用
数据库·mysql
阿猿收手吧!2 小时前
【C++】volatile与线程安全:核心区别解析
java·c++·安全