当前位置: 首页 > news >正文

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

一、前言

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

二、通用接口—文件上传

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

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

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

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

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

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

/*** 通用接口*/
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {//自动装配的是OssConfiguration中创建的含参数的aliOssUtil对象@Autowiredprivate AliOssUtil aliOssUtil;/*** 文件上传* @param file* @return*/@PostMapping("/upload")@ApiOperation("文件上传")public Result<String> upload(MultipartFile file) {log.info("文件上传:{}",file);try {//原始文件名String originalFilename = file.getOriginalFilename();//截取原始文件名的后缀  dfdfdf.pgnString 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文档进行封装的,还是比较简单的:

@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/ObjectNameStringBuilder stringBuilder = new StringBuilder("https://");stringBuilder.append(bucketName).append(".").append(endpoint).append("/").append(objectName);log.info("文件上传到:{}", stringBuilder.toString());return stringBuilder.toString();}
}

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

/*** 用于创建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标记,所以很容易可以写出:

/*** 菜品管理*/
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {@Autowiredprivate DishService dishService;/*** 新增菜品* @param dishDTO* @return*/@PostMapping()@ApiOperation("新增菜品")public Result save(@RequestBody DishDTO dishDTO){log.info("新增菜品:{}",dishDTO);dishService.saveWithFlavor(dishDTO);return Result.success();}
}

DTO如下:

@Data
public class DishDTO implements Serializable {private Long id;//菜品名称private String name;//菜品分类idprivate 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层:

接口:

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

实现类:

@Service
@Slf4j
public class DishServiceImp implements DishService {@Autowiredprivate DishMapper dishMapper;@Autowiredprivate DishFlavorMapper dishFlavorMapper;@Override@Transactionalpublic 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如下:

@Mapper
public interface DishFlavorMapper {/*** 批量插入口味数据* @param flavors*/void insertBatch(List<DishFlavor> flavors);
}
@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来配置:

<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层中处理了逻辑外键问题了)

<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对象。

 /*** 菜品分页查询* @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设计如下:

@Data
public class DishPageQueryDTO implements Serializable {private int page;private int pageSize;private String name;//分类idprivate Integer categoryId;//状态 0表示禁用 1表示启用private Integer status;}

(3)Service层

接口:

/*** 菜品分页查询* @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中的数据),于是响应回前端的数据就变成当前页的了。

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

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

/*** 封装分页查询结果*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {private long total; //总记录数private List records; //当前页数据集合}

(4)持久层

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

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

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

映射文件:

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

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

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

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

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

 <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

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

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

(3)Service层

接口:

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

实现类:

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

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

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

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

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

     /*** 菜品批量删除** @param ids*/@Override@Transactionalpublic 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删菜品。

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

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

/*** 根据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),即找出哪些套餐关联了指定的菜品。

<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到数据库中消耗的时间。

可以优化如下:

        //优化//根据菜品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中动态循环):

/*** 根据菜品id集合批量删除菜品* @param ids*/void deleteByIds(List<Long> ids);
<delete id="deleteByIds">delete from dish where id in<foreach collection="ids" open="(" close=")" separator="," item="id">#{id}</foreach></delete>
/*** 根据菜品id集合批量删除关联的口味数据* @param dishids*/void deleteByDishIds(List<Long> dishids);
 <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层。

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

(3)Service层

接口:

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

实现类:

/*** 修改信息* @param dishDTO*/@Overridepublic 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:

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

映射文件:

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

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

 /*** 根据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);}
 /*** 根据id查询菜品和对应口味* @param id* @return*/DishVO getByIdWithFlavor(Long id);
 /*** 根据id查询菜品和对应口味* @param id* @return*/@Overridepublic DishVO getByIdWithFlavor(Long id) {//根据id查询菜品数据Dish dish = dishMapper.getById(id);//根据菜品id查询口味数据List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id);//将查询到的数据封装到VODishVO dishVO = new DishVO();BeanUtils.copyProperties(dish,dishVO);dishVO.setFlavors(dishFlavors);return dishVO;}
 /*** 根据主键查询菜品* @param id* @return*/@Select("select * from dish where id = #{id}")Dish getById(Long id);

http://www.dtcms.com/a/544336.html

相关文章:

  • 高性能MySql阅读
  • 第3章、MySQL乱码的解决-字符集
  • Ubuntu 安装 Rancher
  • 什么是慢SQL
  • 【人工智能数学基础】多元高斯分布
  • 做网站加入广告联盟做网站的前端是做什么
  • 郑州网页网站制作汕头网站推广优化
  • python电影票房数据可视化分析系统 不同档期电影票房Flask框架 艺恩电影票房网站 requests爬虫(建议收藏)✅
  • webrtc弱网-VivaceUtilityFunction源码分析与算法原理
  • 科技行业ERP系统选择指南:Oracle NetSuite的全面解析
  • 第一个程序HelloWorld
  • 数据分析过程中,发现数值缺失,怎么办?
  • 电商网站设计图海口网站建设好
  • 【自动化测试函数 (上)】Web自动化测试实战精要:定位、操作与窗口管理三部曲
  • 超越传统管理:迈向无感衔接、全域协同的医美运营新范式
  • SUB设备电子狗加密狗开发
  • 1.1 神经网络基本组成
  • HarmonyOS 应用开发:Scroll滚动容器的深度性能优化
  • Java支付对接策略模式详细设计
  • 项目实践6—全球证件智能识别系统(Qt客户端开发+FastAPI后端人工智能服务开发)
  • 微软重磅发布开源引擎Microsoft Agent Framework
  • Qt 高级进阶-MVC架构实现客户端和插件交互(串口案例)
  • 本地部署开源物联网平台 ThingsBoard 并实现外部访问( Windows 版本)
  • leetcode--hot100--思路+知识点(I)
  • 兑吧集团总部大楼乔迁新址 焕新起航开启发展新篇
  • 仓颉视角:ArrayList 动态数组源码深度解析与实践优化
  • 报价网站建设自己动手做一个网页
  • Android红包雨动画效果实现 - 可自定义的扩散范围动画组件
  • codereg.py-ddddocr启动/识别问题
  • 酒泉网站建设有限公司越秀区网站建设公司