一、前言
上一節將員工的CRUD做出來了,同時由於步驟幾乎相同,對於分類的Controller,我們直接導入,就不重複書寫了,接下來就要做菜品的CRUD了,這裏會使用到阿里雲OSS來存儲文件(圖片),同時菜品有不同的口味選擇,所以需要兩個表存儲。
二、通用接口—文件上傳
通用接口中將實現功能實現中公共的方法,這裏我們先只添加文件上傳的方法。
文件上傳的原理就是通過阿里雲OSS來實現雲存儲,這樣可以方便後續菜品的圖片上傳的存儲。
先看看文檔怎麼描述的,很顯然,是通過請求體傳入一個
依舊從上往下書寫,先寫通用接口的Controller,裏面內含文件上傳的方法:
/**
* 通用接口
*/
@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文檔進行封裝的,還是比較簡單的:
@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,便於在接口中自動裝配:
/**
* 用於創建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 {
@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如下:
@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層:
接口:
public interface DishService {
/**
* 新增菜品
* @param dishDTO
*/
public void saveWithFlavor(DishDTO dishDTO);
}
實現類:
@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如下:
@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;
//分類id
private 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
*/
@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());
}
我們自己封裝的分頁查詢結果集如下:
/**
* 封裝分頁查詢結果
*/
@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);
映射文件:
這裏使用的語句就是外連接語句了。
這裏的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
@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刪菜品。
/**
* 根據主鍵查詢菜品
* @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
*/
@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:
/**
* 根據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
*/
@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;
}
/**
* 根據主鍵查詢菜品
* @param id
* @return
*/
@Select("select * from dish where id = #{id}")
Dish getById(Long id);
四、菜品的起售與禁售
1.文檔
請求參數傳來的是一個Path參數和一個Query,Path參數是一個URL中的動態參數,所以我們需要通過@PathVariable來綁定參數,而Query參數直接接收即可,不需要註解來接收參數。
2.Controller
/**
* 菜品啓用禁用
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("菜品啓用禁用")
public Result startOrStop(@PathVariable Integer status,Long id){
log.info("菜品啓用禁用:{},{}",id,status);
dishService.startOrStop(status,id);
return Result.success();
}
3.Service層
接口:
/**
* 菜品啓用禁用
* @param status
* @param id
*/
void startOrStop(Integer status, Long id);
實現類:
實現類這裏比較複雜,如果是停售操作,還需要將包含當前菜品的套餐也停售 。
但是注意,如果我們寫套餐的啓用禁用就不用考慮這麼多了,只需要if上面那一部分即可。
/**
* 菜品禁用啓用
* @param status
* @param id
*/
@Override
@Transactional
public void startOrStop(Integer status, Long id) {
Dish dish = Dish.builder().
status(status).
id(id).
build();
dishMapper.update(dish);
if (status == StatusConstant.DISABLE) {
// 如果是停售操作,還需要將包含當前菜品的套餐也停售
List<Long> dishIds = new ArrayList<>();
dishIds.add(id);
List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(dishIds);
if (setmealIds != null && setmealIds.size() > 0) {
for (Long setmealId : setmealIds) {
Setmeal setmeal = Setmeal.builder()
.id(setmealId)
.status(StatusConstant.DISABLE)
.build();
setmealMapper.update(setmeal);
}
}
}
}
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>