SpringBoot+SSM项目实战 苍穹外卖(3)

news2024/9/29 5:24:41

继续上一节的内容,本节完成菜品管理功能,包括公共字段自动填充、新增菜品、菜品分页查询、删除菜品、修改菜品。

目录

  • 公共字段自动填充
  • 新增菜品
    • 文件上传实现
    • 新增菜品实现 useGeneratedKeys
  • 菜品分页查询
  • 删除菜品
  • 修改菜品
    • 根据id查询菜品实现
    • 修改菜品实现

公共字段自动填充

在上一章节我们已经完成了后台系统的员工管理功能和菜品分类功能的开发,在新增员工或者新增菜品分类时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工或者编辑菜品分类时需要设置修改时间、修改人等字段。如:

新增员工方法:

public void save(EmployeeDTO employeeDTO) {
    .......................
    //设置当前记录的创建时间和修改时间
    employee.setCreateTime(LocalDateTime.now());
    employee.setUpdateTime(LocalDateTime.now());

    //设置当前记录创建人id和修改人id
    employee.setCreateUser(BaseContext.getCurrentId());
    employee.setUpdateUser(BaseContext.getCurrentId());
    .......................
    employeeMapper.insert(employee);
}

修改菜品分类方法:

public void update(CategoryDTO categoryDTO) {
    //....................................
    //设置修改时间、修改人
    category.setUpdateTime(LocalDateTime.now());
    category.setUpdateUser(BaseContext.getCurrentId());
    categoryMapper.update(category);
}

在每一个业务方法中进行操作, 编码相对冗余、繁琐,可以使用AOP切面编程,对于这些公共字段在某个地方统一处理,来简化开发,实现功能增强,来完成公共字段自动填充功能。一共有四个公共字段:

序号字段名含义数据类型操作类型
1create_time创建时间datetimeinsert
2create_user创建人idbigintinsert
3update_time修改时间datetimeinsert、update
4update_user修改人idbigintinsert、update

实现步骤:
1). 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法

2). 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值

3). 在 Mapper 的相应方法上加入 AutoFill 注解

自定义注解 AutoFill

sky-server模块,创建com.sky.annotation包:

/**
 * 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
    //数据库操作类型:UPDATE INSERT
    OperationType value();
}

其中OperationType已在sky-common模块中定义

package com.sky.enumeration;

/**
 * 数据库操作类型
 */
public enum OperationType {

    /**
     * 更新操作
     */
    UPDATE,

    /**
     * 插入操作
     */
    INSERT
}

自定义切面类 AutoFillAspect

在sky-server模块,创建com.sky.aspect包

/**
 * 自定义切面,实现公共字段自动填充处理逻辑
 */
@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){
        log.info("开始进行公共字段自动填充...");

        //获取到当前被拦截的方法上的数据库操作类型
        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){
            return;
        }

        Object entity = args[0];//我们写之前的代码时就已规定好将实体对象放在参数第一位

        //准备赋值的数据
        LocalDateTime now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();

        //根据当前不同的操作类型,为对应的属性通过反射来赋值
        if(operationType == OperationType.INSERT){
            //为4个公共字段赋值
            try {
                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) {
                e.printStackTrace();
            }
        }else if(operationType == OperationType.UPDATE){ //更新操作不需要设置创造时间和创造用户id
            //为2个公共字段赋值
            try {
                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) {
                e.printStackTrace();
            }
        }
    }
}

为了防止类目写错,也为了解耦控制,将方法类目定义为常量:

/**
 * 公共字段自动填充相关常量
 */
public class AutoFillConstant {
    /**
     * 实体类中的方法名称
     */
    public static final String SET_CREATE_TIME = "setCreateTime";
    public static final String SET_UPDATE_TIME = "setUpdateTime";
    public static final String SET_CREATE_USER = "setCreateUser";
    public static final String SET_UPDATE_USER = "setUpdateUser";
}

上面切面类中的代码需要结合注释细读,前置知识比较读多,有面向切面编程、java高级:反射、java高级:注解

完成上述代码后,将员工管理、菜品分类管理的新增和编辑方法中的公共字段赋值的代码注释。并在相应方法的Mapper接口上加入 AutoFill 注解。如CategoryMapper:

@Mapper
public interface CategoryMapper {
    /**
     * 插入数据
     * @param category
     */
    @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(value = OperationType.INSERT)
    void insert(Category category);
    /**
     * 根据id修改分类
     * @param category
     */
    @AutoFill(value = OperationType.UPDATE)
    void update(Category category);
}

功能测试

以新增菜品分类为例,进行测试,启动项目和Nginx,新增菜品分类,通过观察控制台输出的SQL来确定公共字段填充是否完成。category表中数据是否完成自动填充。

在这里插入图片描述

在这里插入图片描述

测试通过,提交代码到git。





新增菜品

添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片。

在这里插入图片描述

业务规则:
菜品名称必须是唯一的
菜品必须属于某个分类下,不能单独存在
新增菜品时可以根据情况选择菜品的口味,口味选项是前端写死的后端无法修改选项
每个菜品必须对应一张图片

接口设计:根据类型查询分类(已完成)
文件上传
新增菜品

请添加图片描述

请添加图片描述

请添加图片描述

新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。所以在新增菜品时,涉及到两个表。

1. 菜品表:dish

字段名数据类型说明备注
idbigint主键自增
namevarchar(32)菜品名称唯一
category_idbigint分类id逻辑外键
pricedecimal(10,2)菜品价格
imagevarchar(255)图片路径
descriptionvarchar(255)菜品描述
statusint售卖状态1起售 0停售
create_timedatetime创建时间
update_timedatetime最后修改时间
create_userbigint创建人id
update_userbigint最后修改人id

2. 菜品口味表:dish_flavor

字段名数据类型说明备注
idbigint主键自增
dish_idbigint菜品id逻辑外键
namevarchar(32)口味名称
valuevarchar(255)口味值

逻辑外键指在建表时没有声明foreignkey,而是在代码上控制逻辑关联而产生的外键。




文件上传实现

本项目选用阿里云的OSS服务进行文件存储。(前面课程已学习过阿里云OSS,不再赘述)

定义OSS相关配置

sky-server模块 application-dev.yml:

这里只是示例,实际配置值需要自己申请阿里云oss并做相应修改。并且现在阿里云oss新版示例代码已经更新为将accessKeyId和accessKeySecret放在环境变量中。

sky:
  alioss:
    endpoint: oss-cn-beijing.aliyuncs.com
    bucket-name: web-tx-36

application.yml:

spring:
  profiles:
    active: dev    #设置环境
sky:
  alioss:
    endpoint: ${sky.alioss.endpoint}
    bucket-name: ${sky.alioss.bucket-name}

注意具体的值没有直接配置在application.yml,其是当前项目的主配置文件,最终项目上线有可能该地方要更改,开发环境和生产环境有可能使用不同账号,所以是采用引用的方式,引用dev,表示开发环境,到时候可能还需要提供一个product即生产环境下的配置文件。到时候只需要把application.yml的active改成product就行了,这也是比较规范的使用方式。

读取OSS配置

sky-common模块中,已定义

@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {
    private String endpoint;
    private String bucketName;
}

注意与application.yml中的书写不是完全一致,它那里使用横向来分割不同单词,而这里使用的是驼峰命名,springboot是会自动转换的,这种方式相对规范,当然如果yml中使用驼峰命名原封不动拷贝过来也是可以的,只不过不符合yml规范。

生成OSS工具类对象

sky-server模块

/**
 * 配置类,用于创建AliOssUtil对象
 */
@Configuration
@Slf4j
public class OssConfiguration {

    @Bean // 项目启动时就会调用这个方法把对象创建出来交给spring容器管理
    @ConditionalOnMissingBean // 保证整个spring容器管理里只有一个AliOssUtil对象
    public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
        log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
        return new AliOssUtil(aliOssProperties.getEndpoint(),
                aliOssProperties.getBucketName());
    }
}

其中,AliOssUtil.java已在sky-common模块中定义

@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {

    private String endpoint;
    private String bucketName;

    /**
     * 文件上传
     *
     * @param bytes
     * @param objectName
     * @return
     */
    public String upload(byte[] bytes, String objectName) {
        OSS ossClient = null;
        try {
            // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
            EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
            // 创建OSSClient实例。
            ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
            // 创建PutObject请求。
            ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } catch (com.aliyuncs.exceptions.ClientException e) {
            System.out.println("环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET获取错误");
            System.out.println("Error Message:" + e.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }

        //文件访问路径规则 https://BucketName.Endpoint/ObjectName
        StringBuilder stringBuilder = new StringBuilder("https://");
        stringBuilder
                .append(bucketName)
                .append(".")
                .append(endpoint)
                .append("/")
                .append(objectName);

        log.info("文件上传到:{}", stringBuilder.toString());

        return stringBuilder.toString();
    }
}

定义文件上传接口

sky-server模块中定义接口

@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {

    @Autowired
    private AliOssUtil aliOssUtil;

    /**
     * 文件上传
     * @param file
     * @return
     */
    @PostMapping("/upload")
    @ApiOperation("文件上传")
    public Result<String> upload(MultipartFile file){
        log.info("文件上传:{}",file);

        try {
            //原始文件名
            String originalFilename = file.getOriginalFilename();
            //截取原始文件名的后缀   dfdfdf.png
            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.error("文件上传失败:{}", e);
        }

        return Result.error(MessageConstant.UPLOAD_FAILED);
    }
}

直接前端测试,上传文件后网站能成功回显图片即成功。

请添加图片描述




新增菜品实现 useGeneratedKeys

设计DTO类

在sky-pojo模块中的DishDTO和DishFlavor

Controller层

sky-server模块

/**
 * 菜品管理
 */
@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();
    }
}

Service层实现类

@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

@AutoFill(value = OperationType.INSERT) // 公共字段填充
void insert(Dish dish);

这里插入口味表时要注意,口味表的数据插入需要对应菜品的id,但是在新增菜品时前端不可能返回菜品的id给我们,因为id也是我们通过insert语句自动生成的,因为我们在建表时设置了id自增,所以我们这里在菜品表的insert里,需要将insert之后生成的菜品id返回,这个动态sql也是可以实现的。通过useGeneratedKeys和keyProperty属性。useGeneratedKeys="true"表示我们需要获得insert之后生成的主键,keyProperty="id"表示希望把主键赋值给我们传入insert语句的参数Dish对象的id属性。

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

void insertBatch(List<DishFlavor> flavors);

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>

功能测试:

进入到菜品管理—>新建菜品,由于没有实现菜品查询功能,所以保存后,暂且在表中查看添加的数据。

请添加图片描述

请添加图片描述

请添加图片描述

测试成功,提交代码。





菜品分页查询

原型图:

在这里插入图片描述

在菜品列表展示时,除了菜品的基本信息(名称、售价、售卖状态、最后操作时间)外,还有两个字段略微特殊,第一个是图片字段 ,我们从数据库查询出来的仅仅是图片的名字,图片要想在表格中回显展示出来,就需要下载这个图片。第二个是菜品分类,这里展示的是分类名称,而不是分类ID,此时我们就需要根据菜品的分类ID,去分类表中查询分类信息,然后在页面展示。

业务规则:根据页码展示菜品信息;每页展示10条数据;分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询

请添加图片描述

设计DTO与VO类

在sky-pojo模块中,已定义DishPageQueryDTO和DishVO

Controller层

根据接口定义创建DishController的page分页查询方法:

@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO) {
    log.info("菜品分页查询:{}", dishPageQueryDTO);
    PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
    return Result.success(pageResult);
}

Service层实现类

在 DishServiceImpl 中实现分页查询方法:

public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
    PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
    Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);//后绪步骤实现
    return new PageResult(page.getTotal(), page.getResult());
}

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>

前端测试:

在这里插入图片描述

测试通过,提交代码。





删除菜品

在菜品列表页面,每个菜品后面对应的操作分别为修改、删除、停售,可通过删除功能完成对菜品及相关的数据进行删除。

在这里插入图片描述

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

请添加图片描述

注意:删除一个菜品和批量删除菜品共用一个接口,故ids可包含多个菜品id,之间用逗号分隔。在进行删除菜品操作时,会涉及到以下三张表。

在这里插入图片描述

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

Controller层

根据删除菜品的接口定义在DishController中创建方法:

@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids) {
    log.info("菜品批量删除:{}", ids);
    dishService.deleteBatch(ids);
    return Result.success();
}

Service层实现类

在DishServiceImpl中实现deleteBatch方法:

@Autowired
private SetmealDishMapper setmealDishMapper;

@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方法,并配置SQL:

@Select("select * from dish where id = #{id}")
Dish getById(Long id);

创建SetmealDishMapper,声明getSetmealIdsByDishIds方法,并在xml文件中编写SQL:

@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);
}

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

在DishMapper.java中声明deleteById方法并配置SQL:

@Delete("delete from dish where id = #{id}")
void deleteById(Long id);

在DishFlavorMapper中声明deleteByDishId方法并配置SQL:

@Delete("delete from dish_flavor where dish_id = #{dishId}")
void deleteByDishId(Long dishId);

使用前后端联调测试,进入到菜品列表查询页面,对测试菜品进行删除操作,同时,进到dish表和dish_flavor两个表查看测试菜品的相关数据都已被成功删除。再次,删除状态为启售的菜品,提示起售中的菜品不能被删除。最后测试批量删除。

在这里插入图片描述

测试通过,提交代码。





修改菜品

在这里插入图片描述

通过对上述原型图进行分析,该页面共涉及4个接口:根据id查询菜品、根据类型查询分类(已实现)、文件上传(已实现)、修改菜品。下面分析这两个接口。

请添加图片描述

请添加图片描述



根据id查询菜品实现

Controller层

根据id查询菜品的接口定义在DishController中创建方法:

@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层实现类

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层

@Select("select * from dish_flavor where dish_id = #{dishId}")
List<DishFlavor> getByDishId(Long dishId);

功能完成后点击修改菜品,能够在前端页面上回显菜品信息:

请添加图片描述



修改菜品实现

Controller层

根据修改菜品的接口定义在DishController中创建方法:

@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishDTO) {
    log.info("修改菜品:{}", dishDTO);
    dishService.updateWithFlavor(dishDTO);
    return Result.success();
}

Service层实现类

在DishServiceImpl中实现updateWithFlavor方法:

@Transactional //事务
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方法:

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

直接前后端联调测试修改菜品信息,修改各个信息进行测试。修改成功提交代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1283882.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Go】Go语言基础内容

变量声明&#xff1a; 变量声明&#xff1a;在Go中&#xff0c;变量必须先声明然后再使用。声明变量使用 var 关键字&#xff0c;后面跟着变量名和类型&#xff0c;如下所示&#xff1a; var age int这行代码声明了一个名为 age 的整数变量。 变量初始化&#xff1a;您可以在声…

JFrog----SBOM清单包含哪些:软件透明度的关键

文章目录 SBOM清单包含哪些&#xff1a;软件透明度的关键引言SBOM清单的重要性SBOM清单包含的核心内容SBOM的创建和管理结论 软件物料清单&#xff08;SBOM&#xff09;是一个在软件供应链安全中越来越重要的组成部分。它基本上是一份清单&#xff0c;详细列出了在特定软件产品…

ENVI植被指数阈值法

植被指数阈值法提取纯净像元 首先用ENVI打开无人机遥感影像 1. 假彩色显示 打开数据管理工具&#xff0c;无人机的4波段为红边波段 2. 波段计算 打开band math&#xff0c;输入 float(b1-b2)/(b1b2) 选择对应波段 3. 阈值筛选 阈值按经验值选的0.7&#xff0c;ndvi…

从零开始实现神经网络(二)_CNN卷积神经网络

参考文章: 介绍卷积神经网络1 介绍卷积神经网络2 在过去的几年里&#xff0c;关于卷积神经网络&#xff08;CNN&#xff09;的讨论很多&#xff0c;特别是因为它们彻底改变了计算机视觉领域。在这篇文章中&#xff0c;我们将建立在神经网络的基本背景知识的基础上&#xff0c;探…

[GPT-1]论文实现:Improving Language Understanding by Generative Pre-Training

Efficient Graph-Based Image Segmentation 一、完整代码二、论文解读2.1 GPT架构2.2 GPT的训练方式Unsupervised pre_trainingSupervised fine_training 三、过程实现3.1 导包3.2 数据处理3.3 模型构建3.4 模型配置 四、整体总结 论文&#xff1a;Improving Language Understa…

android studio 打开flutter项目 出现 dart sdk is not configured

android studio 版本 flutter版本 解决方式 1 点击Open Dart setting 2 打勾Enable Dart support for the project 3 Dart SDK path 选择flutter/bin/cache/dart-sdk 4 打勾Enable Dart support for the following modules

【NI-RIO入门】Real Time(实时系统解释)

1.什么是实时系统&#xff1f; 实时系统可以非常精确可靠的执行需要特定时许要求的系统&#xff0c;对于许多工程项目来说&#xff0c;通用操作系统&#xff0c;例如WINDOWS等标准PC上运行测量和控制程序是无法精确控制计时的&#xff0c;这些系统很容易受用户的其他程序、图像…

【数据结构】——栈|队列(基本功能)

目录 栈 基本概念 栈的常见基本操作 栈的存储 ✌栈的基本操作实现 栈的构建 栈的初始化 入栈 打印栈 出栈 获取栈顶元素 获取栈的有效元素个数 判断栈是否为空 销毁栈 队列 基本概念 队列的常见基本操作 ✌队列的基本操作实现 队列的构建 初始化 入队列 出…

BUUCTF [GXYCTF2019]BabySQli 1 详解!(MD5与SQL之间的碰撞)

题目环境burp抓包 随便输入值 repeater放包 在注释那里发现某种编码 MMZFM422K5HDASKDN5TVU3SKOZRFGQRRMMZFM6KJJBSG6WSYJJWESSCWPJNFQSTVLFLTC3CJIQYGOSTZKJ2VSVZRNRFHOPJ5 看着像是base编码格式 通过测试发现是套加密&#xff08;二次加密&#xff09; 首先使用base32对此编码…

【LeetCode热题100】【双指针】三数之和

给你一个整数数组 nums &#xff0c;判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k &#xff0c;同时还满足 nums[i] nums[j] nums[k] 0 。请 你返回所有和为 0 且不重复的三元组。 注意&#xff1a;答案中不可以包含重复的三元组。 示例 …

C语言 操作符详解

C语言学习 目录 文章目录 前言 一、算术操作符 二、移位操作符 2.1 左移操作符 2.2 右移操作符 三、位操作符 3.1 按位与操作符 & 3.2 按位或操作符 | 3.3 按位异或操作符 ^ 四、赋值操作符 五、单目操作符 5.1 逻辑反操作符&#xff01; 5.2 正值、负值-操作符 5.3 取地址…

老铺黄金IPO:古法黄金的下半场,从高端走向大众?

当前&#xff0c;国内“掘金热”持续走高。据中国黄金协会统计&#xff0c;2023年前三季度全国黄金消费835.07吨&#xff0c;同比增长7.32%。其中黄金首饰552.04吨&#xff0c;同比增长5.72%。 在市场需求带动下&#xff0c;老铺黄金这家专注古法黄金经营的企业今年上半年业绩…

ubuntu 创建conda 环境失败 HTTP 000 CONNECTION FAILED

如有帮助点赞收藏关注&#xff01; 如需转载&#xff0c;请注明出处&#xff01; 现在内存分配好了&#xff0c;创建一个专门的conda环境处理文件&#xff0c;报错了&#xff0c;创建不成功&#xff01; 什么情况&#xff0c;之前明明可以的。 百度吧。 参照一些博客修改了文档…

java synchronized详解

背景 在多线程环境下同时访问共享资源会出现一些数据问题&#xff0c;此关键字就是用来保证线程安全的解决这一问题。 内存可见的问题 在了解synchronized之前先了解一下java内存模型&#xff0c;如下图&#xff1a; 线程1去主内存获取x的值读入本地内存此时x的值为1&…

Linux(14):进程管理

一个程序被加载到内存当中运作&#xff0c;那么在内存内的那个数据就被称为进程(process)。 进程是操作系统上非常重要的概念&#xff0c;所有系统上面跑的数据都会以进程的型态存在。 进程 在 Linux底下所有的指令与能够进行的动作都与权限有关&#xff0c;而系统如何判定权…

大数据技术学习笔记(四)—— HDFS

目录 1 HDFS 概述1.1 HDFS 背景与定义1.2 HDFS 优缺点1.3 HDFS 组成架构1.4 HDFS 文件块大小 2 HDFS的shell操作2.1 上传2.2 下载2.3 HDFS直接操作 3 HDFS的客户端操作3.1 Windows 环境准备3.2 获取 HDFS 的客户端连接对象3.3 HDFS文件上传3.4 HDFS文件下载3.5 HDFS删除文件和目…

Vue项目解决van-calendar 打开下拉框显示空白(白色),需滑动一下屏幕,才可正常显示

问题描述&#xff0c;如图 ipad(平板&#xff09;或者 H5移动端引入Vant组件的日历组件&#xff08;van-calendar&#xff09;&#xff0c;初始化显示空白&#xff0c;需滚动一下屏幕&#xff0c;才可正常显示 解决方法 需在van-calendar上绑定open"openCalendar"事件…

升辉清洁IPO:广东清洁服务“一哥”还需要讲好全国化的故事

近日&#xff0c;广东物业清洁服务“一哥”升辉清洁第四次冲击IPO成功&#xff0c;拟于12月5日在香港主板挂牌上市。自2021年4月第一次递交招股书&#xff0c;时隔两年半&#xff0c;升辉清洁终于拿到了上市的门票。 天眼查显示&#xff0c;升辉清洁成立于2000年&#xff0c;主…

基于SpringBoot+Vue的前后端分离的房屋租赁系统2

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 开发过程中&#xff0…

最强AI之风袭来,你爱了吗?

2017年&#xff0c;柯洁同阿尔法狗人机大战&#xff0c;AlphaGo以3比0大获全胜&#xff0c;一代英才泪洒当场...... 2019年&#xff0c;换脸哥视频“杨幂换朱茵”轰动全网&#xff0c;时至今日AI换脸仍热度只增不减&#xff1b; 2022年&#xff0c;ChatGPT一经发布便轰动全球&a…