苍穹外卖——菜品/套餐管理

公共字段自动填充

        在我们的业务表中,有很多相同的字段,如创建人、创建时间、修改人、修改时间等等字段,大多数业务都需对这些字段进行赋值,这就会造成大量重复的代码。

        create_time、create_user两字段需要在insert方法中使用,update_user、update_time两字段需要在insert、update方法中使用。
        做好分类后我们就可以借助切面来统一进行处理。

一、自定义注解@AutoFill,用于标识需要进行公共字段填充的方法。

        我们选择在server模块新建一个包annotation用于存放注解AutoFill,然后添加对应的注解@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME)。

        再在该自定义注解中定义枚举类属性来判断当前操作的类型,可以定义方法value(),返回类型为事先存在的枚举类OperationType,意为使用@AutoFill注解时都需传入一个OperationType枚举类型的值(注解中的方法不需要写方法体)。

@Target(ElementType.METHOD)//指定该注解只能用于方法上
@Retention(RetentionPolicy.RUNTIME)//指定注解的生命周期
public @interface AutoFill {
    // 数据库操作类型:UPDATE INSERT
    // 定义方法,要求使用@AutoFill注解时必须提供一个OperationType枚举类型的值
    // 这个值用于指示方法执行时的操作类型
    OperationType value();
}

二、自定义切面类AutoFillAspect,统一拦截加入了@AutoFill注解的方法

        在server模块新建一个包aspect用于存放切面类.AutoFillAspect,然后添加注解@Aspect @Component @Slf4j。

        在该切面类中定义需要拦截的方法为mapper包下所有被@AutoFill注解标记的方法。并执行前置通知来填充字段。

        来看前置通知,首先需获取拦截方法所需的操作类型并判断是update还是insert,可以通过调用连接点对象的getSignature()方法来获得签名,签名为接口,但拦截到的为方法,我们可向下转型为其子接口MethodSignature,然后利用反射,调用getMethod().getAnnotation(AutoFill.class)来获得方法上的注解对象,再通过获取注解的值来获取操作类型。

        然后需要获取被拦截的方法的参数,即实体对象。我们仍通过调用连接点对象的getArgs()来获取参数,其会返回包含所有的参数的数组,我们通过Object[] args来接收,而为方便代码编写,我们约定要将实体类放在第一个位置,因此args[0]就是实体类,我们通过Object实例来接收(注意不要使用Employee实例接收,因此后期可能其他模块也使用自动填充,而Employee接收会报错)。同时为了防止空指针,我们对args数组添加非空判断。

        再准备赋值的数据,修改时间直接获取当前时间即可,当前用户ID则调用BaseContext类中的方法getCurrentId()来获取。

        最后根据当前不同的操作类型,为对应的属性通过反射来赋值。这里调用getClass().getMethod()方法时传入的第一个参数应该为方法名,但可能会出现打错字母导致找不到对应方法的情况,因此我们选择调用common模块constant包AutoFillConstant类中的常量字符串使代码更规范

@Aspect
@Component
@Slf4j
public class AutoFillAspect {
    /*
     * 切入点
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..))&& @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut() {
    }
    /*
      前置通知,进行公共字段赋值
     */
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint) throws Throwable {

        // 获取当前被拦截方法的信息,包括方法名、参数类型等
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 从方法上获取@AutoFill注解,这个注解定义了操作类型(如插入或更新)
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
        // 获取注解中定义的操作类型
        OperationType operationType = autoFill.value();

        // 获取被拦截方法的参数列表,约定第一个参数是实体对象
        Object[] args = joinPoint.getArgs();
        // 如果没有参数或参数列表为空,则直接返回
        if (args == null || args.length == 0) {
            return;
        }
        // 约定第一个参数是实体对象
        Object object = args[0];

        // 准备自动填充的数据
        LocalDateTime now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId(); // 获取当前用户ID,通常是从上下文中获取

        // 根据不同的操作类型(插入或更新),使用反射为实体对象的属性赋值
        if (operationType == OperationType.INSERT) {
            // 如果是插入操作,需要为四个字段赋值:创建时间、创建人、更新时间、更新人
            Method setCreateTime = object.getClass().getMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
            Method setCreateUser = object.getClass().getMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
            Method setUpdateTime = object.getClass().getMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
            Method setUpdateUser = object.getClass().getMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

            //通过反射为对象属性赋值
            setCreateTime.invoke(object, now);
            setCreateUser.invoke(object, currentId);
            setUpdateTime.invoke(object, now); // 插入时,更新时间和创建时间通常相同
            setUpdateUser.invoke(object, currentId); // 插入时,更新人和创建人通常相同
        } else if (operationType == OperationType.UPDATE) {
            // 如果是更新操作,只需要为两个字段赋值:更新时间和更新人
            Method setUpdateTime = object.getClass().getMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
            Method setUpdateUser = object.getClass().getMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

            //通过反射为对象属性赋值
            setUpdateTime.invoke(object, now);
            setUpdateUser.invoke(object, currentId);
        }
    }
}

三、为Mapper层中所有需要拦截的方法加上自定义注解@AutoFill并指定操作类型(包括刚刚复制的文件中的包含更新和插入的方法)

@Mapper
public interface EmployeeMapper {
    @Insert("insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user) " +
            "values " +
            "(#{name},#{username}, #{password},#{phone},#{sex}, #{idNumber}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
    @AutoFill(value = OperationType.INSERT)
    void insert(Employee employee);

    @AutoFill(value = OperationType.UPDATE)
    void update(Employee employee);
}

        此时就可以删除原本接口实现类中用于赋值的代码,前置通知会完成这一部分。 

——菜品模块————————

新增菜品

        该业务需遵守的规则:

        接口应分为三个:分类查询、文件上传、新增菜品。其中分类查询请求路径为/admin/category/list,请求方法为get,上文已实现在此不再赘述。

文件上传

        文件上传功能需要先配置alioss,同样的参数名不再使用硬编码的方式编写,而是引用common模块properties包AliOssProperties类中定义的参数(该类上的注解@ConfigurationProperties(prefix = "sky.alioss")使该类成为配置属性类)。
        注意在类中我们采用驼峰命名法:accessKeyId,但在配置文件中以"-"连接:access-key-id,springboot框架会自动对其进行转换,当然在配置文件中仍使用驼峰命名法也可行。
        之前我们在application.yml中直接配置具体的值,但开发到上线使用需要修改相关信息,比较麻烦,我们采用引用的方式:在application-dev.yml文件中配置具体值(该文件适合在开发环境下使用),投入使用时prod文件会取代他,届时我们只需将主配置文件中spring.profiles.active的属性修改为为prod即可。

  • application-dev.yml(开发环境配置)
  • application-prod.yml(生产环境配置)
  • application.yml(主配置文件)

        引用方式为:空格${其他配置文件中属性名},原代码中数据库的相关配置信息也是这样实现的(之前自行编写的AliOssUtil和这次给出的AliOssUtil内容不一样,对返回的图片url的处理方式也不一致,因此endpoint: https://oss-cn-beijing.aliyuncs.com需要去掉https://):

//application.yml文件————————————————————————————
spring:
  profiles:
    active: dev #当前激活的配置文件是开发环境(dev)的配置文件
sky:
  jwt:
    admin-secret-key: itcast
    admin-ttl: 604800000
    admin-token-name: token
    #阿里云配置
  alioss:
    access-key-id:  ${sky.alioss.access-key-id} #注意":"后面需有一个空格
    access-key-secret: ${sky.alioss.access-key-secret}
    endpoint: ${sky.alioss.endpoint}
    bucket-name: ${sky.alioss.bucket-name}
//application-dev.yml文件————————————————————————
sky:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    host: localhost
    port: 3306
    database: sky_take_out
    username: root
    password: 1234
  alioss:
    access-key-id: LTAI5t6nivgHXQ1rnBt3dudV  # 阿里云OSS的Access Key ID
    access-key-secret: EhrGz86soycvHNnK0V4PuZoDYgu4tm  # 阿里云OSS的Access Key Secret
    endpoint: oss-cn-beijing.aliyuncs.com  # 阿里云OSS的端点
    bucket-name: chn-webapp  # 阿里云OSS的Bucket名称

         在controller层创建CommonController,在该类中创建方法upload()用于响应路径为/admin/common/upload的post方法,同时注入AliOssUtil类的bean,在该方法中引用common模块utils包AliOssUtil类upload()方法,其会返回该图片的访问网址。
        但AliOssUtil类中定义的四个属性endpoint、accessKeyId、accessKeySecret、bucketName尚未初始化,可以在server模块config包中新建OSSConfiguration类,同时添加@Configuration注解将其作为配置类,此时@Configuration修饰的自动配置类通常会在项目启动时就被创建。

//AliOssProperties——————————————————————————————————
@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {
    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;
}
//AliOssUtil——————————————————————————————————————————
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {
    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;
    // 文件上传方法,返回值为访问图片的url
    public String upload(byte[] bytes, String objectName) {
    //略
    }
}
//OSSConfiguration——————————————————————————————————————————
//配置类,用于创建AliOssUtils对象
@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()
        );
    }
}

        执行顺序为dev配置文件—yml配置文件—AliOssProperties文件获取相关信息并生成Bean—OSSConfiguration类在方法aliOssUtil的参数中获取bean实例—方法体内将AliOssProperties实例的值赋给AliOssUtil实例并返回—将OSSConfiguration转为bean。

        再来回到CommonController的upload()方法中,其引用的AliOssUtil类upload()方法需要传入两个参数,一个为byte数组,一个为String。其中byte[]可以将接收的图片参数调用file.getBytes()转为数组,String为图片的名称,为了防止文件重名导致覆盖,我们可以使用uuid来重命名,获取UUID后后缀的过程不再赘述(需调用UUID.randomUUID().toString(),之前未调用toString()方法,不严谨)。

@RestController
@RequestMapping("/admin/common")
@Slf4j
@Api("通用接口")
public class CommonController {
    @Autowired
    private AliOssUtil aliOssUtil;
    /**
     * 文件上传
     * @param file 1
     * @return 1
     */
    @PostMapping("/upload")
    @ApiOperation("文件上传")
    public Result<String> upload(MultipartFile file) throws IOException {//参数与前端保持一致,必须是file
        log.info("文件上传:{}",file);
        //获取原始文件名
        String originalFilename = file.getOriginalFilename();
        //截取原始文件名的后缀
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
        //构建新文件名称
        String objectName= UUID.randomUUID().toString()+suffix;

        //获取文件的访问路径
        String filePath=aliOssUtil.upload(file.getBytes(), objectName);
        return Result.success(filePath);
    }
}

        接下来测试,因为需要上传图片,所以swagger接口文档并不适合,我们选择直接前后端联调,图片成功回显即为代码正确。

新增菜品

        请求路径为/admin/dish,请求方法为post,参数为DishDTO类来接收前端提交的json数据,该类最后一个属性为private List<DishFlavor> flavors = new ArrayList<>();代表口味。因此在Impl中需要将DishDTO分为Dish dish和List<DishFlavor> flavors。

        在SetmealServiceImpl中因为需要向套餐表和套餐菜品关系表两张表插入数据,因此需要注解@Transactional将当前方法交给spring进行事务管理。

        在插入dish时,因为sql支持批量插入,因此不再每条数据依次插入,而是批量插入数据。同时因为flavors的dishId只有在插入dish数据后才能得到,因此我们修改插入dish的语句为

<insert id="insert" useGeneratedKeys="true" keyProperty="id">

        其中useGeneratedKeys="true"告诉MyBatis使用JDBC的getGeneratedKeys方法来检索数据库生成的主键值(例如,自增主键)。keyProperty="id"指定了MyBatis应该将检索到的主键值设置到传入对象的id属性中。然后在DishServiceImpl类中,给flavors赋值前获取dish的id,并遍历flavors集合插入对应的id。

        在三层中逐层新建类和接口并编写代码:

//Controller———————————————————
@RestController
@RequestMapping("/admin/dish")
@Slf4j
@Api(tags = "菜品管理")
public class DishController {
    @Autowired
    private DishService dishService;

    @PostMapping
    @ApiOperation("新增菜品")
    public Result save(@RequestBody DishDTO dishDTO) {
        dishService.saveWithFlavor(dishDTO);
        return Result.success();
    }
}
//Service———————————————————————
public interface DishService {
    public void saveWithFlavor(DishDTO dishDTO);
}
//ServiceImpl———————————————————
@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {

    @Autowired
    private SetmealMapper setmealMapper;
    @Autowired
    private SetmealDishMapper setmealDishMapper;
    @Autowired
    private DishMapper dishMapper;

    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     * @param setmealDTO
     */
    @Transactional
    public void saveWithDish(SetmealDTO setmealDTO) {
        Setmeal setmeal = new Setmeal();// 创建套餐对象
        BeanUtils.copyProperties(setmealDTO, setmeal);// 将DTO中的属性复制到套餐对象中

        //向套餐表插入数据
        setmealMapper.insert(setmeal);// 调用Mapper插入套餐数据到数据库

        //获取生成的套餐id
        Long setmealId = setmeal.getId();
        // 获取套餐关联的菜品列表
        List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
        setmealDishes.forEach(setmealDish -> {
            setmealDish.setSetmealId(setmealId);// 为每个关联菜品设置套餐ID
        });

        //保存套餐和菜品的关联关系
        setmealDishMapper.insertBatch(setmealDishes);// 批量插入套餐和菜品的关联数据到数据库
    }
}
//Mapper———————————————————————
@Mapper
public interface DishFlavorMapper {
    void insertBatch(List<DishFlavor> flavors);
}
@Mapper
public interface DishMapper {
    @Select("select count(id) from dish where category_id = #{categoryId}")
    Integer countByCategoryId(Long categoryId);

    @AutoFill(value = OperationType.INSERT)//公共字段填充,操作类型为insert
    void insert(Dish dish);
}

<?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="fla" separator=",">
            (#{fla.dishId},#{fla.name},#{fla.value})
        </foreach>
    </insert>
</mapper>


<?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">
    <!-- useGeneratedKeys="true"告诉MyBatis使用JDBC的getGeneratedKeys方法来检索数据库生成的主键值(例如,自增主键)。
    keyProperty="id"指定了MyBatis应该将检索到的主键值设置到传入对象的id属性中。-->
        insert into sky_take_out.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>

菜品分页查询

        前面已经完成了员工的分页查询和菜品分类的分页查询,菜品的分页查询也类似,来看要求:根据页码展示菜品信息、每页展示10条数据、可根据需要传入菜品分类、菜品名称、菜品状态进行查询。

        请求路径为/admin/dish/page,请求方法为get,传参里page和pagesize为必须,其他三者name、categoryId、status为可选参数。请求方式为Query,不需要@RequestBody。

        使用DishPageQueryDTO类来接收,因为返回数据中有一项categoryName为分类名称,其在dish表中并不存在,需查询category表后返回对应的数据,因此在Impl中查询的返回类型为DishVO。查询的sql语句为:

select dish.*, category.name as categoryName
from dish
         left outer join category on dish.category_id = category.id

        其中limit 0,10之类的语句不需要写,已由PageHelper完成。因为dish表和category表中都含有name字段,且DishVO中与分类名称对应的字段为categoryName,因此需要给category.name 添加语句as categoryName来起别名。

//Controller———————————————————
    @GetMapping("/page")
    @ApiOperation("菜品分类查询")
    public Result<PageResult> page( DishPageQueryDTO dishPageQueryDTO) {
        log.info("菜品分页查询:{}", dishPageQueryDTO);
        PageResult pageResult=dishService.pageQuery(dishPageQueryDTO);
        return Result.success(pageResult);
    }
//Service———————————————————————
    PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);
//ServiceImpl———————————————————
    @Override
    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());
    }
//Mapper———————————————————————
    Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);
    <select id="pageQuery" resultType="com.sky.vo.DishVO">
        select dish.*, category.name as categoryName
        from dish
                 left outer join category on dish.category_id = category.id
        <where>
            <if test="name!=null">and dish.name like concat('%',#{name},'%')</if>
            <if test="categoryId != null">and dish.category_id = #{categoryId}</if>
            <if test="status != null">and dish.status = #{status}</if>
        </where>
        order by dish.create_time desc
    </select>

删除菜品

         业务规则:可以删除单个菜品,也可以批量删除多个菜品。同时正在售卖(status为1)的菜品不可删除。被套餐关联的菜品不能删除。删除菜品后,相关联的口味数据也需要删除。

        请求路径为/admin/dish,请求方法为delete,传参ids为各个菜品id,中间以","分隔,请求方式为Query,不需要@RequestBody。

        执行删除对应id的菜品和口味时,最基础的方法是对每个id执行一次删除两者的操作,但因为传入的id可能为多个,一旦多起来会生成大量的sql语句,拖慢运行效率。因此我们选择使用动态SQL,传入一个列表,再xml映射文件中使用foreach进行删除,这样仅需两个SQL语句即可完成删除。

//Controller———————————————————
    @DeleteMapping
    @ApiOperation("批量删除套餐")
    public Result delete(@RequestParam List<Long> ids){
        setmealService.deleteByIds(ids);
        return Result.success();
    }
//Service———————————————————————
    void deleteByIds(List<Long> ids);
//ServiceImpl———————————————————
    @Transactional
    @Override
    public void deleteByIds(List<Long> ids) {
        ids.forEach(id -> {
            Setmeal setmeal = setmealMapper.getById(id);
            if(setmeal.getStatus()== StatusConstant.ENABLE) {
                throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
            }});
        SetmealMapper.deleteByIds(ids);
        SetmealDishMapper.deleteByIds(ids);
    }
//Mapper———————————————————————
public interface DishMapper {
    @Select("select * from dish where id=#{id}")
    Dish getById(Long id);

    @Delete("delete from dish where id=#{id}")
    void deleteById(Long id);//老版,这里仅仅做示范,不再使用

    void deleteByIds(List<Long> ids);
}

public interface SetMealDishMapper {
    List<Long> getSetMealIdsByDishIds(List<Long> dishIds);
}

public interface DishFlavorMapper {
    void deleteByIds(List<Long> dishIds);
}
<mapper namespace="com.sky.mapper.DishMapper">
    <delete id="deleteByIds">
        delete from dish where id in
        <foreach collection="ids" item="id" separator="," open="(" close=")">
            #{id}
        </foreach>
    </delete>
</mapper>

<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>

<mapper namespace="com.sky.mapper.DishFlavorMapper">
    <delete id="deleteByIds">
        delete from dish_flavor where dish_id in
        <foreach collection="dishIds" item="dishId" separator="," open="(" close=")">
            #{dishId}
        </foreach>
    </delete>
</mapper>

修改菜品

        其大致与新增菜品的需求类似,但多了一项根据ID查询回显的功能。涉及到的接口较多,例如根据id查询菜品信息用于回显、根据菜品查询分类用于回显、上传图片的接口、保存菜品信息的接口。因为中间两接口已实现,因此只需实现第一、第四个接口即可。

根据id查询菜品

        请求路径为/admin/dish/{id},请求方法为get。返回类型为DishVO装在Result对象中。

        先查询对应ID的菜品,因为上文在删除菜品判断能否删除—是否正在售卖部分已经编写了getById()方法,此处不需要编写,将查询到的dish值通过BeanUtils.copyProperties(dish,dishVO)拷贝到DishVO对象中,此时除了categoryName和flavors都已获取到,categoryName在删除页面的分页查询中就已获取,不需要补充。

        根据菜品ID查询到的口味可能为多个,因此使用List接收泛型为DishFlavor,调用dishVO.setFlavors(dishFlavors)将数据赋给dishVO即可。然后将dishVO返回。

//Controller———————————————————
    @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———————————————————————
    DishVO getByIdWithFlavor(Long id);
//ServiceImpl———————————————————
    @Override
    public DishVO getByIdWithFlavor(Long id) {
        //根据ID查询菜品数据
        Dish dish = dishMapper.getById(id);//之前已在dishMapper中编写了getById()方法
        //根据菜品ID查询口味数据
        List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id);
        //将查询到的数据封装进DishVO
        log.info(dishFlavors.toString());
        DishVO dishVO = new DishVO();
        BeanUtils.copyProperties(dish,dishVO);//将获取到的Dish拷贝到dishVO中
        dishVO.setFlavors(dishFlavors);
        return dishVO;
    }
//Mapper———————————————————————
    @Select("select * from dish_flavor where dish_id=#{dishId}")
    List<DishFlavor> getByDishId(Long DishId);//为便于理解这里id替换为DishId

修改菜品

        请求路径为/admin/dish,请求方法为put。

        获取ID后,分为修改菜品和口味两部分。

        修改菜品部分,因为前端传入的为DishDTO对象,有数据不需要使用,因此选择新建Dish对象,并将数据拷入,然后作为参数传入update()方法。因为修改涉及公共字段自动填充,因此在Mapper层的方法需添加注解@AutoFill(value = OperationType.UPDATE)。同时因为各参数不一定有值,我们选择在xml文件中使用动态SQL。

        改口味较复杂,分三种情况:增加口味、删除口味、不变,依次编写较为复杂,我们选择直接删除原有的数据,然后插入新的数据。删除口味所需的deleteByDishId()方法上文已实现,直接调用,新增口味同样也已存在,直接调用insertBatch()方法并添加相关逻辑。

//Controller———————————————————
    @PutMapping
    @ApiOperation("修改菜品")
    public Result update(@RequestBody DishDTO dishDTO) {
        dishService.upadateDishAndFlavor(dishDTO);
        return Result.success();
    }
//Service———————————————————————
    void upadateDishAndFlavor(DishDTO dishDTO);
//ServiceImpl———————————————————
    @Transactional
    @Override
    public void update(SetmealDTO setmealDTO) {
        //更新套餐基本信息
        Setmeal setmeal = new Setmeal();
        BeanUtils.copyProperties(setmealDTO, setmeal);
        setmealMapper.update(setmeal);
        //删除原有套餐内菜品信息
        Long setmealId = setmealDTO.getId();
        setmealDishMapper.deleteBySetmealId(setmealId);
        //重新向套餐内添加菜品信息
        List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
        setmealDishes.forEach(setmealDish -> {
            setmealDish.setSetmealId(setmealId);
        });
        setmealDishMapper.insertBatch(setmealDishes);
    }
//Mapper———————————————————————
    @Delete("delete from setmeal_dish where setmeal_id=#{setmealId}")
    void deleteBySetmealId(Long setmealId);

菜品起售/停售

        请求路径为/admin/setmeal/status/{status},请求方法为post。

        在Impl中,我们可以直接调用之前编写的dishMapper.update()方法,该方法要求传入dish对象,因此我们使用build()将传入的status和id赋值。

        同时注意,如果是停售操作,包含当前菜品的套餐也需停售,因此我们还需判断菜品的状态是否为停售,如果是则将套餐也停售,这涉及到了多张数据表的操作,需要加入注解@Transactional保持其一致性。

        上文已在SetmealDishMapper中编写了getSetMealIdsByDishIds()方法用于查询对应id的套餐,可以直接调用,但其参数要求是数组,我们需新建一数组并将id传入数组中。

        该方法总计所需修改菜品、根据菜品ID查询套餐、修改套餐三步,其中前两步已有实现方法,直接调用即可,重点编写第三步:

//Controller———————————————————
    @ApiOperation("菜品起售、停售")
    @PostMapping("/status/{status}")
    public Result<String> updateStatus(@PathVariable Integer status, Long id) {
        dishService.updateStatus(status,id);
        return Result.success();
    }
//Service———————————————————————
    void updateStatus(Integer status, Long id);
//ServiceImpl———————————————————
    @Transactional//因为涉及多张表所以需开启事务管理
    @Override
    public void updateStatus(Integer status, Long id) {
        Dish dish = Dish.builder()
                .status(status)
                .id(id)
                .build();
        dishMapper.update(dish);

        // 如果是停售操作,还需要将包含当前菜品的套餐也停售
        if (status == StatusConstant.DISABLE) {
            //getSetMealIdsByDishIds()方法要求传参List,构建List并将id传入
            List<Long> dishIds = new ArrayList<>();
            dishIds.add(id);
            // 查询包含当前菜品的多个套餐ID,并放入列表中
            List<Long> setMealIds = setmealDishMapper.getSetMealIdsByDishIds(dishIds);
            // 如果存在相关套餐,则进行停售处理
            if (setMealIds != null && setMealIds.size() > 0) {
                for (Long setmealId : setMealIds) {
                    // 构建Setmeal对象并设置状态和ID
                    Setmeal setmeal = Setmeal.builder()
                            .id(setmealId)
                            .status(StatusConstant.DISABLE)
                            .build();
                    // 更新套餐信息
                    setmealMapper.update(setmeal);
                }
            }
        }
    }
//Mapper———————————————————————
@Mapper
public interface SetmealMapper {
    @AutoFill(OperationType.UPDATE)
    void update(Setmeal setmeal);
}
<mapper namespace="com.sky.mapper.SetmealMapper">

    <update id="update" parameterType="Setmeal">
        update setmeal
        <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="status != null">status = #{status},</if>
            <if test="description != null">description = #{description},</if>
            <if test="image != null">image = #{image},</if>
            <if test="updateTime != null">update_time = #{updateTime},</if>
            <if test="updateUser != null">update_user = #{updateUser}</if>
        </set>
        where id = #{id}
    </update>
</mapper>

——套餐模块————————

新增套餐

        该业务需遵守的规则:

  • 套餐名称唯一
  • 套餐必须属于某个分类
  • 套餐必须包含菜品
  • 名称、分类、价格、图片为必填项
  • 添加菜品窗口需要根据分类类型来展示菜品
  • 新增的套餐默认为停售状态

        接口应分为四个:根据类型查询分类(已完成)、根据分类id查询菜品、图片上传(已完成)、新增套餐。

根据分类ID查询菜品

        请求路径为/admin/dish/list,请求方法为get,前端传入分类ID,后端使用Long categoryId接收。同时返回值为多个菜品,防在一个集合内,因此方法返回类型为:Result<List<Dish>>。

        在三层中逐层新建类和接口并编写代码:

//Controller———————————————————
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<Dish>> list(Long categoryId){
    List<Dish> list = dishService.list(categoryId);
    return Result.success(list);
}
//Service———————————————————————
List<Dish> list(Long categoryId);
//ServiceImpl———————————————————
public List<Dish> list(Long categoryId) {
    // 使用Builder模式构建Dish对象,设置查询条件
    Dish dish = Dish.builder()
        .categoryId(categoryId)
        .status(StatusConstant.ENABLE)
        .build();
    return dishMapper.list(dish);
}
//Mapper———————————————————————
List<Dish> list(Dish dish);
    <select id="list" resultType="Dish" parameterType="Dish">
        select * from dish
        <where>
            <if test="name != null">and name like concat('%',#{name},'%')</if>
            <if test="categoryId != null">and category_id = #{categoryId}</if>
            <if test="status != null">and status = #{status}</if>
        </where>
        order by create_time desc
    </select>

新增套餐

        请求路径为/admin/setmeal,请求方法为post,参数为SetmealDTO类来接收前端提交的json数据。与新增菜品类似,该类最后一个属性为private List<SetmealDish> setmealDishes包含多个关联菜品SetmealDishe。因此在Impl中需要将SetmealDTO分为Setmeal setmeal和List<SetmealDish> setmealDishes分别用来装套餐信息和套餐里的多个菜品。

        先向套餐表插入数据,然后修改DishMapper.xml的insert方法resultType="Dish" parameterType="Dish"以便获取套餐的ID,再将套餐ID依次插入setmealDishe中为每个关联菜品设置套餐ID。最后使用setmealDishMapper.insertBatch()向套餐菜品关系表批量插入数据。同样,因为对两个表进行了操作,需要加入@Transactional注解。

        在DishServiceImpl中因为需要向菜品表和口味表两张表插入数据,因此需要注解@Transactional将当前方法交给spring进行事务管理。因此是插入操作,还需在SetmealMapper的insert方法上 添加注解@AutoFill(OperationType.INSERT),菜品套餐关系表中无creat_time等信息,因此不需要添加。

        在三层中逐层新建类和接口并编写代码:

//Controller———————————————————
@RestController
@RequestMapping("/admin/setmeal")
@Api(tags = "套餐相关接口")
@Slf4j
public class SetmealController {

    @Autowired
    private SetmealService setmealService;

    @PostMapping
    @ApiOperation("新增套餐")
    public Result save(@RequestBody SetmealDTO setmealDTO) {
        setmealService.saveWithDish(setmealDTO);
        return Result.success();
    }
}
//Service———————————————————————
public interface SetmealService {
    void saveWithDish(SetmealDTO setmealDTO);
}
//ServiceImpl———————————————————
@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {

    @Autowired
    private SetmealMapper setmealMapper;
    @Autowired
    private SetmealDishMapper setmealDishMapper;
    @Autowired
    private DishMapper dishMapper;

    @Transactional
    public void saveWithDish(SetmealDTO setmealDTO) {
        Setmeal setmeal = new Setmeal();
        BeanUtils.copyProperties(setmealDTO, setmeal);
        //向套餐表插入数据
        setmealMapper.insert(setmeal);
        //获取生成的套餐id
        Long setmealId = setmeal.getId();
        List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
        setmealDishes.forEach(setmealDish -> {
            setmealDish.setSetmealId(setmealId);
        });
        //保存套餐和菜品的关联关系
        setmealDishMapper.insertBatch(setmealDishes);
    }
}
//Mapper———————————————————————
@Mapper
public interface SetmealMapper {
    @AutoFill(OperationType.INSERT)
    void insert(Setmeal setmeal);
}
@Mapper
public interface SetmealDishMapper {
    void insertBatch(List<SetmealDish> setmealDishes);
}
<mapper namespace="com.sky.mapper.SetmealMapper">
    <insert id="insert" parameterType="Setmeal" useGeneratedKeys="true" keyProperty="id">
        insert into setmeal
        (category_id, name, price, status, description, image, create_time, update_time, create_user, update_user)
        values (#{categoryId}, #{name}, #{price}, #{status}, #{description}, #{image}, #{createTime}, #{updateTime},
                #{createUser}, #{updateUser})
    </insert>
</mapper>

<mapper namespace="com.sky.mapper.SetmealDishMapper">
    <insert id="insertBatch" parameterType="list">
        insert into setmeal_dish
        (setmeal_id,dish_id,name,price,copies)
        values
        <foreach collection="setmealDishes" item="sd" separator=",">
            (#{sd.setmealId},#{sd.dishId},#{sd.name},#{sd.price},#{sd.copies})
        </foreach>
    </insert>
</mapper>

 套餐分页查询

        请求路径为/admin/setmeal/page,请求方法为get,传参里page和pagesize为必须,其他三者name、categoryId、status为可选参数。请求方式为Query,不需要@RequestBody。

        使用SetmealPageQueryDTO类来接收,因为返回数据中有一项categoryName为分类名称,其在setmeal表中并不存在,需查询category表后返回对应的数据,因此因此在Impl中查询的返回类型为SetmealVO。

//Controller———————————————————
@GetMapping("/page")
@ApiOperation("分页查询")
public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO) {
    PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
    return Result.success(pageResult);
}
//Service———————————————————————
public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {
    int pageNum = setmealPageQueryDTO.getPage();
    int pageSize = setmealPageQueryDTO.getPageSize();

    PageHelper.startPage(pageNum, pageSize);
    Page<SetmealVO> page = setmealMapper.pageQuery(setmealPageQueryDTO);
    return new PageResult(page.getTotal(), page.getResult());
}
//Mapper———————————————————————
Page<SetmealVO> pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);
    <select id="pageQuery" resultType="com.sky.vo.SetmealVO">
        select setmeal.*, category.name
        from setmeal left join category
        on setmeal.category_id = category.id
        <where>
            <if test="name != null">and setmeal.name like concat('%', #{name}, '%')</if>
            <if test="status != null">and setmeal.status = #{status}</if>
            <if test="categoryId != null">and setmeal.category_id = #{categoryId}</if>
        </where>
        order by setmeal.create_time desc
    </select>

删除套餐

         业务规则:可以删除单个套餐,也可以批量删除多个套餐。同时正在售卖(status为1)的套餐不可删除。删除套餐后,还需根据套餐id删除套餐和菜品的关联关系。

        请求路径为/admin/setmeal,请求方法为delete,传参ids为各个套餐id,中间以","分隔,请求方式为Query,不需要@RequestBody。

        需要三步:查询该id的套餐能否被删除、删除套餐、删除套餐菜品关系表中的数据

//Controller———————————————————
@DeleteMapping
@ApiOperation("批量删除套餐")
public Result delete(@RequestParam List<Long> ids){
    setmealService.deleteBatch(ids);
    return Result.success();
}
//Service———————————————————————
void deleteBatch(List<Long> ids);
//ServiceImpl———————————————————
    @Transactional
    @Override
    public void deleteByIds(List<Long> ids) {
        ids.forEach(id -> {
            Setmeal setmeal = setmealMapper.getById(id);
            if(setmeal.getStatus()== StatusConstant.ENABLE) {
                throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
            }});
        setmealMapper.deleteByIds(ids);
        setmealDishMapper.deleteByIds(ids);
    }
//Mapper———————————————————————
public interface SetmealMapper {
    @Select("select * from setmeal where id=#{id}")
    Setmeal getById(Long id);

    void deleteByIds(List<Long> ids);
}

public interface SetMealDishMapper {
    void deleteByIds(List<Long> ids) ;
}
<mapper namespace="com.sky.mapper.SetmealMapper">
    <delete id="deleteByIds">
        delete from setmeal where id in
        <foreach collection="ids" item="id" separator="," open="(" close=")">
            #{id}
        </foreach>
    </delete>
</mapper>

<mapper namespace="com.sky.mapper.SetmealDishMapper">
    <delete id="deleteByIds">
        delete from setmeal_dish where id in
        <foreach collection="ids" item="id" separator="," open="(" close=")">
            #{id}
        </foreach>
    </delete>
</mapper>

修改套餐

        与修改菜品类似涉及到的接口较多,例如根据id查询套餐、根据类型查询分类(已完成)、根据分类id查询菜品(已完成)、图片上传(已完成)、修改套餐。同样只需实现因此只需实现第一、第五个接口即可。

根据id查询套餐

        请求路径为/admin/setmeal/{id},请求方法为get。返回类型为SetmealVO装在Result对象中。

        先查询对应ID的套餐,将查询到的dish值通过BeanUtils.copyProperties(dish,dishVO)拷贝到DishVO对象中,此时除了categoryName和flavors都已获取到,categoryName在删除页面的分页查询中就已获取,不需要补充。

        再查询对应ID的套餐内的菜品,使用List接收泛型为SetmealDish,将数据赋给setmealVO并返回。

//Controller———————————————————
    @GetMapping("/{id}")
    @ApiOperation("根据id查询套餐")
    public Result<SetmealVO> getById(@PathVariable Long id) {
        SetmealVO setmealVO=setmealService.getById(id);
        return Result.success(setmealVO);
    }
//Service———————————————————————
    SetmealVO getById(Long id);
//ServiceImpl———————————————————
    @Override
    public SetmealVO getById(Long id) {
        Setmeal setmeal = setmealMapper.getById(id);
        List<SetmealDish> setmealDishes = setmealDishMapper.getBySetmealId(id);
        SetmealVO setmealVO = new SetmealVO();
        BeanUtils.copyProperties(setmeal, setmealVO);
        setmealVO.setSetmealDishes(setmealDishes);
        return setmealVO;
    }
//Mapper———————————————————————
    @Select("select * from setmeal_dish where setmeal_id=#{setmealId}")
    List<SetmealDish> getBySetmealId(Long setmealId);

修改菜品

        请求路径为/admin/setmeal,请求方法为put。

        获取ID后,分为修改套餐和套餐内菜品两部分。

        修改菜品部分,因为前端传入的为SetmealDTO对象,有数据不需要使用,因此选择新建Setmeal对象并将数据拷入,然后作为参数传入update()方法。

        改套餐内菜品我们依然先全部删除,然后重新添加。

//Controller———————————————————
    @PutMapping
    @ApiOperation("修改套餐")
    public Result update(@RequestBody SetmealDTO setmealDTO) {
        setmealService.update(setmealDTO);
        return Result.success();
    }
//Service———————————————————————
    void update(SetmealDTO setmealDTO);
//ServiceImpl———————————————————
    @Transactional
    @Override
    public void update(SetmealDTO setmealDTO) {
        //更新套餐基本信息
        Setmeal setmeal = new Setmeal();
        BeanUtils.copyProperties(setmealDTO, setmeal);
        setmealMapper.update(setmeal);
        //删除原有套餐内菜品信息
        Long setmealId = setmealDTO.getId();
        setmealDishMapper.deleteBySetmealId(setmealId);
        //重新向套餐内添加菜品信息
        List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
        setmealDishes.forEach(setmealDish -> {
            setmealDish.setSetmealId(setmealId);
        });
        setmealDishMapper.insertBatch(setmealDishes);
    }
//Mapper———————————————————————
    @Delete("delete from setmeal_dish where setmeal_id=#{setmealId}")
    void deleteBySetmealId(Long setmealId);

套餐起售/停售

        请求路径为/admin/dish/status/{status},请求方法为post。

        如果是启售操作,需先检查套餐内菜品是否都处于起售状态,这里三个if分别判断"是否是启售操作"、"是否查询到菜品"、"菜品状态是否为停售"。

//Controller———————————————————
    @PostMapping("/status/{status}")
    @ApiOperation("套餐起售、停售")
    public Result<String> updateStatus(@PathVariable Integer status, Long id) {
        setmealService.updateStatus(status, id);
        return Result.success();
    }
//Service———————————————————————
    void updateStatus(Integer status, Long id);
//ServiceImpl———————————————————
    @Override
    public void updateStatus(Integer status, Long id) {
        //起售套餐需先检查套餐内是否存在停售菜品
        if(status == StatusConstant.ENABLE){
            List<Dish> dishList = dishMapper.getBySetmealId(id);
            if(dishList != null && dishList.size() > 0){
                dishList.forEach(dish -> {
                    if (dish.getStatus() == StatusConstant.DISABLE) {
                        throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED);//有停售菜品提示"套餐内包含未启售菜品,无法启售"
                    }
                });
            }
        }

        Setmeal setmeal = Setmeal.builder()
                .id(id)
                .status(status)
                .build();
        setmealMapper.update(setmeal);
    }
//Mapper———————————————————————
public interface DishMapper {
    @Select("select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = #{setmealId}")
    List<Dish> getBySetmealId(Long id);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值