苍穹外卖 —— 文件上传和菜品的CRUD

一、前言

上一节将员工的CRUD做出来了,同时由于步骤几乎相同,对于分类的Controller,我们直接导入,就不重复书写了,接下来就要做菜品的CRUD了,这里会使用到阿里云OSS来存储文件(图片),同时菜品有不同的口味选择,所以需要两个表存储。

二、通用接口---文件上传

对于文件上传部分遗忘的,可以在这一篇文章中看到:

SpringMVC ------ 响应和请求处理-CSDN博客

通用接口中将实现功能实现中公共的方法,这里我们先只添加文件上传的方法。

文件上传的原理就是通过阿里云OSS来实现云存储,这样可以方便后续菜品的图片上传的存储。

先看看文档怎么描述的,很显然,是通过请求体传入一个

依旧从上往下书写,先写通用接口的Controller,里面内含文件上传的方法:

java 复制代码
/**
 * 通用接口
 */
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {

    //自动装配的是OssConfiguration中创建的含参数的aliOssUtil对象
    @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.pgn
            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);
    }
}

对于这个方法,我们的目的是通过工具类将指定文件上传到阿里云,我们从请求体中接收一个MultipartFile(二进制的文件类型参数),若上传成功,最后将返回一个文件路径的字符串到data,若上传失败,将返回报错结果集到msg。

这里对于文件名是进行了处理的,使用的是UUID来对文件进行随机命名(结合多种元素如时间戳、随机数等),但是由于我们依旧需要扩展名,所以要先将后缀分离出来,然后将文件名部分处理,最后拼接在一起。

最终得到类似的文件名:

OSS的工具类如下,这是基于阿里云官网给出的Java文档进行封装的,还是比较简单的:

java 复制代码
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

    /**
     * 文件上传
     *
     * @param bytes
     * @param objectName
     * @return
     */
    public String upload(byte[] bytes, String objectName) {

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
            // 创建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());
        } 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();
    }
}

OSS的自动装配的配置类如下,目的是创建aliOssUtil的bean,便于在接口中自动装配:

java 复制代码
/**
 * 用于创建AliOssUtil对象
 */
@Configuration
@Slf4j
public class OssConfiguration {

    @Bean
    @ConditionalOnMissingBean//只要没有这个Bean就创建
    public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
        log.info("开始创建阿里云文件上传工具对象{}",aliOssProperties);
        return new AliOssUtil(
                aliOssProperties.getEndpoint(),
                aliOssProperties.getAccessKeyId(),
                aliOssProperties.getAccessKeySecret(),
                aliOssProperties.getBucketName());
    }
}

三、菜品的CRUD

1.新增菜品

(1)文档

老样子,先看文档:

(2)Controller

可以看到这里需要接收请求体中的参数,所以直接就能想到用DTO接收,并且用@RequestBody标记,所以很容易可以写出:

java 复制代码
/**
 * 菜品管理
 */
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {

    @Autowired
    private DishService dishService;

    /**
     * 新增菜品
     * @param dishDTO
     * @return
     */
    @PostMapping()
    @ApiOperation("新增菜品")
    public Result save(@RequestBody DishDTO dishDTO){
        log.info("新增菜品:{}",dishDTO);
        dishService.saveWithFlavor(dishDTO);
        return Result.success();
    }
}

DTO如下:

java 复制代码
@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<>();

}

值得注意的是,这里我们使用了一个数组集合来储存口味选项,因为一个菜品的口味可能有很多个,所以自然的,我们需要一个新的表来存储这个数据,这个表中由dish_id来作为逻辑外键,与dish表的主键进行关联(这样就知道哪几个口味属于哪一个菜品了):

同时dish也需要一个表来存储:

(3)Service层

接下来看Service层:

接口:

java 复制代码
public interface DishService {

    /**
     * 新增菜品
     * @param dishDTO
     */
    public void saveWithFlavor(DishDTO dishDTO);
}

实现类:

java 复制代码
@Service
@Slf4j
public class DishServiceImp implements DishService {

    @Autowired
    private DishMapper dishMapper;

    @Autowired
    private DishFlavorMapper dishFlavorMapper;

    @Override
    @Transactional
    public void saveWithFlavor(DishDTO dishDTO) {
        Dish dish = new Dish();

        BeanUtils.copyProperties(dishDTO, dish);

        dishMapper.insert(dish);

        //获取Insert语句生成的主键值
        Long dishId = dish.getId();

        //向口味表中插入n条数据
        List<DishFlavor> flavors = dishDTO.getFlavors();
        if (flavors != null && flavors.size() > 0){
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishId);
            });
            //向口味表插入n条数据
            dishFlavorMapper.insertBatch(flavors);
        }
    }
}

这里需要注意了,这个与员工的新增就不太一样了,首先先插入dish到菜品表中,这一点是一样的。

但是这里我们需要额外处理口味表:由于是用一个数组集合存储口味的,所以这里需要遍历口味表来将每个口味对应的dish_id设置为当前插入的菜品的id(逻辑外键),最终才将这些口味插入到口味表中去。

对应关系可以看看下图:

(4)持久层

两个表的mapper如下:

java 复制代码
@Mapper
public interface DishFlavorMapper {

    /**
     * 批量插入口味数据
     * @param flavors
     */
    void insertBatch(List<DishFlavor> flavors);
}
java 复制代码
@Mapper
public interface DishMapper {


    /**
     * 根据分类id查询菜品数量
     * @param categoryId
     * @return
     */
    @Select("select count(id) from dish where category_id = #{categoryId}")
    Integer countByCategoryId(Long categoryId);


    /**
     * 新增菜品和对应口味
     * @param dish
     */
    @AutoFill(value = OperationType.INSERT)
    void insert(Dish dish);
}

由于新增操作涉及插入的变量较多,我们就不使用注解了,这里使用xml来配置:

XML 复制代码
<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>

这里是用了动态sql语句的,将flavors集合遍历,插入表中(先前已经在Service层中处理了逻辑外键问题了)

XML 复制代码
<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>

这里有两个参数比较陌生:

1. useGeneratedKeys="true":

作用:告知 MyBatis,当前插入操作的主键是由数据库自动生成的(例如 MySQL 的 AUTO_INCREMENT

2. keyProperty="id":

作用:指定将数据库生成的主键值,赋值到 Java 对象的哪个属性上。

2.分页查询菜品

分页查询依旧需要使用到PageHelper。

(1)文档

先观察文档

很容易发现分页查询是用的Query参数,这就代表需要用到分页插件了,返回值是查询到的内容。

(2)Controller

有了员工的查询经验,这里很容易写出菜品的分页查询,这里由于使用到了分页插件,所以需要专门创建一个DTO来存储数据,Controller内容很简单,先日志,然后调用Service层,最后返回结果集,由于我们需要分好了页的结果,所以这里传到结果集中的是pageResult对象。

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

        return Result.success(pageResult);
    }

DTO设计如下:

java 复制代码
@Data
public class DishPageQueryDTO implements Serializable {

    private int page;

    private int pageSize;

    private String name;

    //分类id
    private Integer categoryId;

    //状态 0表示禁用 1表示启用
    private Integer status;

}

(3)Service层

接口:

java 复制代码
/**
     * 菜品分页查询
     * @param dishPageQueryDTO
     * @return
     */
    PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);

**实现类:**实现类中调用了PageHelper插件,传入DTO,这个DTO其实是前端发送过来的请求体,这里面包含了很多参数,包括了页码和每页记录数(所以这个DTO也与普通的DTO不一样),这将作为startPage方法传入的参数用于开启分页。

然后我们需要调用持久层获取查询结果,结果是用VO封装的(VO是前端展示数据,DTO是前端请求后端的),最终我们返回的VO结果集还需要封装到我们自己创建的PageResult中去,然后传给Controller。

其实这里可以理解为当我们按下下一页按钮时,前端就会重新发出一个请求,这时新DTO的参数就会改变成当前页的,于是开启分页时的参数也改变了,当然从持久层拿出的结果VO也会变,于是封装VO的结果集也变了,我们自己封装结果集的PageResult当然也会变,最后的结果就是在Controller中传回的响应结果也变了(响应回去的data中的数据),于是响应回前端的数据就变成当前页的了。

java 复制代码
 /**
     * 菜品分页查询
     *
     * @param dishPageQueryDTO
     * @return
     */
    @Override
    public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
        PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
        Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
        return new PageResult(page.getTotal(), page.getResult());
    }

我们自己封装的分页查询结果集如下:

java 复制代码
/**
 * 封装分页查询结果
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {

    private long total; //总记录数

    private List records; //当前页数据集合

}

(4)持久层

前面也提到了,持久层在分页中的作用就是拿出VO结果,我们要清晰的知道目的,不然会被各种封装绕晕。

**Mapper:**由于使用到动态语句,所以这里还是使用xml文件配置。

java 复制代码
 /**
     * 菜品分页查询
     * @param dishPageQueryDTO
     * @return
     */
    Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);

映射文件:

这里使用的语句就是外连接语句了。

可以到这篇文章中复习一下:

MYSQL ------ 约束和多表查询-CSDN博客

这里的sql语句很复杂,先要搞清楚目的,再看这条语句就会觉得豁然开朗了。

这里的目的是:通过动态条件查询 dish 表的菜品信息,并关联(通过id关联) category 表获取菜品对应的分类名称(通过外连接),最终将结果封装到 DishVO 实体类中,支持按名称模糊查询、按分类 ID 筛选、按状态筛选,并按创建时间倒序排列(最新创建的菜品在前)

java 复制代码
 <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>

3.批量删除菜品

(1)文档

先看文档:

这里请求的参数是**ids,**而且是String类型的,这样我们很不好处理,好在Spring很智能,可以将这个请求参数自动转化为一个集合,所以这里我们必然会使用到@RequestParam注解,返回值没有要求,就是返回个成功结果集就行了。

(2)Controller

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

没什么好说的,用了刚刚的注解,将请求参数转换成了集合。

(3)Service层

接口:

java 复制代码
     /**
     * 菜品批量删除
     * @param ids
     */
    void deleteBatch(List<Long> ids);

实现类:

这里要说一下,我们的删除和批量删除是做到一起的,因为批量删除完全可以完成删除的功能,完全可以复用。

来看具体的要求:起售的菜品肯定不能删,和套餐关联的菜品肯定也不能删,条件都满足,就可以删,所以我们采用了以下的逻辑:

遍历前端传来的集合(转换后),每次循环都从持久层拿一个对应id的对象,判断起售状态,不满足就抛异常,满足就继续判断是否被套餐关联。

然后去套餐表中查是否有这个id,如果有就抛异常,没有就进行删除操作。

删除操作是需要循环的,确保每个id对应的菜品都被删除。

java 复制代码
     /**
     * 菜品批量删除
     *
     * @param ids
     */
    @Override
    @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);
        }  

    }

(4)持久层

下面两个简单,直接上注解,一个是用id查菜品,一个是用id删菜品。

java 复制代码
    /**
     * 根据主键查询菜品
     * @param id
     * @return
     */
    @Select("select * from dish where id = #{id}")
    Dish getById(Long id);
java 复制代码
/**
     * 根据主键删除菜品数据
     * @param id
     */
    @Delete("delete from dish where id = #{id}")
    void deleteById(Long id);

但是从套餐中查id就不能直接用注解了,因为需要动态sql语句,我们需要遍历套餐表,去查找在中间表中,是否对应的菜品id有对应的套餐id。

java 复制代码
/**
     * 根据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);

目的:通过传入的多个菜品 ID(dishIds),查询出所有包含这些菜品的套餐 ID(setmeal_id),即找出哪些套餐关联了指定的菜品。

XML 复制代码
<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>

这三个表关系如图:

(5)优化

最后一步删除操作是可以优化的,将循环在数据库中进行,节省从Java到数据库中消耗的时间。

可以优化如下:

java 复制代码
        //优化
        //根据菜品id集合批量删除菜品数据
        //sql: delete from dish where id in (?,?,?)
        dishMapper.deleteByIds(ids);

        //根据菜品id集合批量删除关联的口味数据
        //sql: delete from dish_flavor where dish_id in (?,?,?)
        dishFlavorMapper.deleteByDishIds(ids);

分别在菜品和口味的Mapper中添加优化的查找方式(在sql中动态循环):

java 复制代码
/**
     * 根据菜品id集合批量删除菜品
     * @param ids
     */
    void deleteByIds(List<Long> ids);
XML 复制代码
<delete id="deleteByIds">
        delete from dish where id in
        <foreach collection="ids" open="(" close=")" separator="," item="id">
            #{id}
        </foreach>
    </delete>
java 复制代码
    /**
     * 根据菜品id集合批量删除关联的口味数据
     * @param dishids
     */
    void deleteByDishIds(List<Long> dishids);
XML 复制代码
 <delete id="deleteByDishIds" >
        delete from dish_flavor where dish_id
        <foreach collection="dishIds" open="(" close=")" separator="," item="dishId">
            #{dishId}
        </foreach>
    </delete>

4.修改菜品

没有什么要注意的,别忘记修改时选项要回显就行(相当于又是一个接口,查询接口)。

(1)文档

修改操作注意需要做到数据的回显,所以可以看到请求体中向后端带来了全部参数,最终只需要返回成功结果集即可。

(2)Controller

用DTO接收请求体中的数据,然后调用Service层。

java 复制代码
 /**
     * 修改菜品
     * @param dishDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改菜品")
    public Result update(@RequestBody DishDTO dishDTO){
        log.info("修改菜品");
        dishService.updateWithFlavor(dishDTO);
        return Result.success();
    }

(3)Service层

接口:

java 复制代码
    /**
     * 根据id修改菜品和口味信息
     * @param dishDTO
     */
    void updateWithFlavor(DishDTO dishDTO);

实现类:

java 复制代码
/**
     * 修改信息
     * @param dishDTO
     */
    @Override
    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);
        }

    }

(4)持久层

Mapper:

java 复制代码
    /**
     * 根据id动态修改菜品
     * @param dish
     */
    @AutoFill(value = OperationType.UPDATE)
    void update(Dish dish);

映射文件:

XML 复制代码
 <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>

(5)回显

原理是点下修改按钮,就会跳转页面,同时向后端发起请求参数id,后端根据id查询菜品和口味,最后从后端返回VO集合(由于是要向前端展示的,所以用的VO)。

虽然也算一个完整接口,但是很简单,所以给出代码即可。

java 复制代码
 /**
     * 根据id查询菜品
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public Result<DishVO> getById(@PathVariable Long id){
        log.info("根据id查询菜品:{}",id);
        DishVO dishVO = dishService.getByIdWithFlavor(id);
        return Result.success(dishVO);
    }
java 复制代码
 /**
     * 根据id查询菜品和对应口味
     * @param id
     * @return
     */
    DishVO getByIdWithFlavor(Long id);
java 复制代码
 /**
     * 根据id查询菜品和对应口味
     * @param id
     * @return
     */
    @Override
    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;
    }
java 复制代码
 /**
     * 根据主键查询菜品
     * @param id
     * @return
     */
    @Select("select * from dish where id = #{id}")
    Dish getById(Long id);
相关推荐
Java陈序员3 小时前
完全开源!一款基于 SpringBoot + Vue 构建的社区平台!
vue.js·spring boot·github·社区
bcbnb3 小时前
Fiddler抓包实战教程 从安装配置到代理设置,详解Fiddler使用方法与调试技巧(HTTPHTTPS全面指南)
后端
.柒宇.3 小时前
《云岚到家》第一章个人总结
spring boot·spring·spring cloud
颜颜yan_3 小时前
Rust impl块的组织方式:从基础到实践的深度探索
开发语言·后端·rust
xiguolangzi3 小时前
mysql迁移PG库 主键、唯一处理、批量修改
java·后端
刘一说3 小时前
深入掌握 Spring Boot Web 开发:构建高性能 RESTful API 的最佳实践
前端·spring boot·restful
摇滚侠3 小时前
Spring Boot3零基础教程,Actuator 导入,笔记82
java·spring boot·笔记
DolphinScheduler社区3 小时前
小白指南:Apache DolphinScheduler 补数据功能实操演示
java·大数据·开源·apache·海豚调度·大数据工作流调度
TDengine (老段)3 小时前
TDengine 数据函数 TAN 用户手册
java·大数据·数据库·物联网·时序数据库·tdengine·涛思数据