1.公共字段填充
1.1 問題分析
????????在新增員工或者新增菜品分類時需要設置創建時間、創建人、修改時間、修改人等字段,在編輯員工或者編輯菜品分類時需要設置修改時間、修改人等字段。這些字段屬于公共字段,也就是也就是在我們的系統中很多表中都會有這些字段,如下:
序號 | 字段名 | 含義 | 數據類型 |
---|---|---|---|
1 | create_time | 創建時間 | datetime |
2 | create_user | 創建人id | bigint |
3 | update_time | 修改時間 | datetime |
4 | update_user | 修改人id | bigint |
????????而針對于這些字段,目前,在我們的項目中處理這些字段都是在每一個業務方法中進行賦值操作,重復代碼過多,很冗雜,可以使用AOP切面編程 實現公共字段的自動填充
1.2 實現思路
- 自定義注解 AutoFill,用于標識需要進行公共字段自動填充的方法
- 自定義切面類 AutoFillAspect,統一攔截加入了 AutoFill 注解的方法,通過反射為公共字段賦值
- 在 Mapper 的方法上加入 AutoFill 注解
1.3 代碼開發
1.2.1?自定義注解 AutoFill
????????AutoFill類中定義用于標識需要進行公共字段自動填充的方法
- @Target注解指定該注解可以標注在方法上,用于標識哪些方法需要進行自動填充操作
- @Retention 注解定義該注解的生命周期為運行時(RUNTIME),確保可以通過反射獲取到該注解信息
package com.sky.annotatin;import com.sky.enumeration.OperationType;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 自定義注解,用于標識需要自動填充的字段* @Target 注解指定該注解可以標注在方法上,用于標識哪些方法需要進行自動填充操作* @Retention 注解定義該注解的生命周期為運行時(RUNTIME),確保可以通過反射獲取到該注解信息*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
// 數據庫操作類型:insert、updateOperationType value();}
1.2.2 自定義切面類 AutoFillAspect
????????統一攔截加入了 AutoFill 注解的方法,通過反射為公共字段賦值
定義切點:攔截 com.sky.mapper 包下所有方法,并且要求方法上必須有 @AutoFill 注解
定義前置通知:參數是切入點
package com.sky.aspect;import com.sky.annotatin.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;
import java.time.LocalDate;
import java.time.LocalDateTime;@Aspect
@Component
@Slf4j
public class AutoFillAspect {/*** 切入點* 定義切點:攔截 com.sky.mapper 包下所有方法,并且要求方法上必須有 @AutoFill 注解*/@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotatin.AutoFill)")public void autoFillPointCut(){}/*** 前置通知* @param joinPoint*/@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){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){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();}}}
}
? ? ? ? 為了避免字符串出錯,在constant包下定義了一個AutoFillConstant?類,定義實體類中的方法名稱。
package com.sky.constant;/*** 公共字段自動填充相關常量*/
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";
}
1.2.3 在 Mapper 的方法上加入 AutoFill 注解
- EmployeeMapper
- CategoryMapper
? ? ? ? 將impl實現類中補全創建人、創建時間、修改人、修改時間的代碼注掉?
/*** 新增分類* @param categoryDTO*/public void save(CategoryDTO categoryDTO) {Category category = new Category();//屬性拷貝BeanUtils.copyProperties(categoryDTO, category);//分類狀態默認為禁用狀態0category.setStatus(StatusConstant.DISABLE);//設置創建時間、修改時間、創建人、修改人
// category.setCreateTime(LocalDateTime.now());
// category.setUpdateTime(LocalDateTime.now());
// category.setCreateUser(BaseContext.getCurrentId());
// category.setUpdateUser(BaseContext.getCurrentId());categoryMapper.insert(category);}/*** 修改分類* @param categoryDTO*/public void update(CategoryDTO categoryDTO) {Category category = new Category();BeanUtils.copyProperties(categoryDTO,category);//設置修改時間、修改人
// category.setUpdateTime(LocalDateTime.now());
// category.setUpdateUser(BaseContext.getCurrentId());categoryMapper.update(category);}
1.4 功能測試
? ? ? ? 重啟項目,登錄系統新增修改員工進行測試:
1.5 將代碼推送到遠程倉庫
2. 新增菜品
2.1 需求分析與設計
2.1.1 產品原型
后臺系統中可以管理菜品信息,通過 新增功能來添加一個新的菜品,在添加菜品時需要選擇當前菜品所屬的菜品分類,并且需要上傳菜品圖片。
新增菜品原型:
業務規則:
-
菜品名稱必須是唯一的
-
菜品必須屬于某個分類下,不能單獨存在
-
新增菜品時可以根據情況選擇菜品的口味
-
每個菜品必須對應一張圖片
2.1.2 接口設計
接口設計:
-
根據類型查詢分類(已完成)
-
文件上傳
-
新增菜品
接下來需要分析每個接口,明確每個接口的請求方法、請求路徑、傳入參數以及返回值。
- 根據類型查詢分類
- 文件上傳
- 新增菜品
2.1.3 表設計
通過原型圖進行分析:
????????新增菜品,其實就是將新增頁面錄入的菜品信息插入到dish表,如果添加了口味做法,還需要向dish_flavor表插入數據。所以在新增菜品時,涉及到兩個表:
表名 | 說明 |
---|---|
dish | 菜品表 |
dish_flavor | 菜品口味表 |
1). 菜品表:dish
字段名 | 數據類型 | 說明 | 備注 |
---|---|---|---|
id | bigint | 主鍵 | 自增 |
name | varchar(32) | 菜品名稱 | 唯一 |
category_id | bigint | 分類id | 邏輯外鍵 |
price | decimal(10,2) | 菜品價格 | |
image | varchar(255) | 圖片路徑 | |
description | varchar(255) | 菜品描述 | |
status | int | 售賣狀態 | 1起售 0停售 |
create_time | datetime | 創建時間 | |
update_time | datetime | 最后修改時間 | |
create_user | bigint | 創建人id | |
update_user | bigint | 最后修改人id |
2). 菜品口味表:dish_flavor
字段名 | 數據類型 | 說明 | 備注 |
---|---|---|---|
id | bigint | 主鍵 | 自增 |
dish_id | bigint | 菜品id | 邏輯外鍵 |
name | varchar(32) | 口味名稱 | |
value | varchar(255) | 口味值 |
2.2 代碼開發
? ? ? ? 查詢分類的接口已經實現過,所以只需要開發文件上傳和新增菜品兩個功能
2.2.1 文件上傳實現
???????因為在新增菜品時,需要上傳菜品對應的圖片(文件),包括后緒其它功能也會使用到文件上傳,故要實現通用的文件上傳接口。
????????文件上傳,是指將本地圖片、視頻、音頻等文件上傳到服務器上,可以供其他用戶瀏覽或下載的過程。文件上傳在項目中應用非常廣泛,我們經常發抖音、發朋友圈都用到了文件上傳功能。
?????????在本項目選用阿里云的OSS服務進行文件存儲
1)首先登錄阿里云,按以下操作創建一個文件上傳的空間
?
?
?2)定義OSS相關配置
????????application.yml
sky:jwt:# 設置jwt簽名加密時使用的秘鑰admin-secret-key: itcast# 設置jwt過期時間admin-ttl: 7200000# 設置前端傳遞過來的令牌名稱admin-token-name: tokenalioss:endpoint: ${sky.alioss.endpoint}access-key-id: ${sky.alioss.access-key-id}access-key-secret: ${sky.alioss.access-key-secret}bucket-name: ${sky.alioss.bucket-name}
????????application-dev.yml
注意:如果Bucket位于北京地域,請使用 oss-cn-beijing.aliyuncs.com
sky:datasource:driver-class-name: com.mysql.cj.jdbc.Driverhost: localhostport: 3306database: sky_take_outusername: rootpassword: 1234alioss:endpoint: oss-cn-beijing.aliyuncs.comaccess-key-id: LTAI5txxxxxxxxxxm5CSJEPdaccess-key-secret: WLfGxxxxxxxxxxxxvKpZ58clAcTnT6Lbucket-name: sky-xiaoyang
3). 讀取OSS配置
在sky-common模塊中,已定義
package com.sky.properties;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {private String endpoint;private String accessKeyId;private String accessKeySecret;private String bucketName;}
4). 生成OSS工具類對象
? ? ? ? 在common.utils.AliOssUtil類中將endpoint、accessKeyId、accessKeySecret、bucketName賦好值,在upload方法中進行拼接,返回得到的就是上傳文件的路徑
package com.sky.utils;import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;@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();}
}
? ? ? ? 但是現在數據都是空的,需要定義一個配置類,用于創建AliOssUtil對象,在OssConfiguration 中定義:
package com.sky.config;import com.sky.properties.AliOssProperties;
import com.sky.utils.AliOssUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** 配置類,用于創建AliOssUtil對象*/
@Configuration
@Slf4j
public class OssConfiguration {/*** 創建AliOssUtil實例的配置方法* 該方法會在Spring容器中不存在AliOssUtil類型的Bean時生效* 通過注入的AliOssProperties參數獲取阿里云OSS配置信息,創建并返回AliOssUtil實例** @param aliOssProperties 包含阿里云OSS配置信息的參數對象* @return 配置好的AliOssUtil實例*/@Bean@ConditionalOnMissingBeanpublic AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) {log.info("開始創建阿里云文件上傳工具類對象:{}", aliOssProperties);return new AliOssUtil(aliOssProperties.getEndpoint(),aliOssProperties.getAccessKeyId(),aliOssProperties.getAccessKeySecret(),aliOssProperties.getBucketName());}
}
我整理了一張截圖,方便大家理解
4)CommonController類編寫請求
package com.sky.controller.admin;import com.sky.constant.MessageConstant;
import com.sky.result.Result;
import com.sky.utils.AliOssUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;import java.io.IOException;
import java.util.UUID;@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {@Autowiredprivate AliOssUtil aliOssUtil;@PostMapping("/upload")@ApiOperation("文件上傳")public Result<String> upload(MultipartFile file) {log.info("文件上傳:{}", file);try {//1.獲取原始文件String originalFilename = file.getOriginalFilename();
// 獲取后綴名String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
// 構建新文件名String name = UUID.randomUUID().toString() + extension;// 文件的請求路徑String filePath = aliOssUtil.upload(file.getBytes(), name);return Result.success(filePath);} catch (IOException e) {log.info("文件上傳失敗:{}",e);}//2.返回訪問路徑return Result.error(MessageConstant.UPLOAD_FAILED);}
}
? ? ? ? 啟動項目,進行測試,是可以存儲到阿里云OSS的
? ? ? ? 注意:可能會出現圖片可以存儲到阿里云OSS,但是前端頁面回顯是出現錯誤的,這是因為我們在創建桶的時候默認是組織公共訪問的,需要修改成公共訪問,就可以成功回顯了?
?
2.2.2 新增菜品
1). 設計DTO類
在sky-pojo模塊中
package com.sky.dto;import com.sky.entity.DishFlavor;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;@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<>();}
2). Controller層
????????DishController
package com.sky.controller.admin;import com.sky.dto.DishDTO;
import com.sky.result.Result;
import com.sky.service.DishService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** 菜品管理*/
@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();}
}
3). Service層接口
package com.sky.service;import com.sky.dto.DishDTO;
import com.sky.entity.Dish;public interface DishService {/*** 新增菜品和對應的口味** @param dishDTO*/public void saveWithFlavor(DishDTO dishDTO);}
4). Service層實現類
package com.sky.service.impl;import com.sky.dto.DishDTO;
import com.sky.entity.Dish;
import com.sky.entity.DishFlavor;
import com.sky.mapper.DishFlavorMapper;
import com.sky.mapper.DishMapper;
import com.sky.service.DishService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.List;@Service
public class DishServiceImpl implements DishService {@Autowiredprivate DishMapper dishMapper;@Autowiredprivate DishFlavorMapper dishFlavorMapper;@Transactionalpublic void saveWithFlavor(DishDTO dishDTO) {Dish dish = new Dish();
// 屬性copyBeanUtils.copyProperties(dishDTO, dish);//向菜品表插入1條數據dishMapper.insert(dish);//后緒步驟實現//獲取insert語句生成的主鍵值Long dishId = dish.getId();List<DishFlavor> flavors = dishDTO.getFlavors();if (flavors != null && flavors.size() > 0) {
// 給每一個口味設置菜品idflavors.forEach(dishFlavor -> {dishFlavor.setDishId(dishId);});//向口味表插入n條數據dishFlavorMapper.insertBatch(flavors);//后緒步驟實現}}
}
5). Mapper層
- ????????DishMapper.java中添加 @AutoFill(value = OperationType.INSERT)
/*** 插入菜品數據** @param dish*/@AutoFill(value = OperationType.INSERT)void insert(Dish dish);
- ????????在/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"><!-- 新增之后生成的id會傳給dish --><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.java
package com.sky.mapper;import com.sky.entity.DishFlavor;
import java.util.List;@Mapper
public interface DishFlavorMapper {/*** 批量插入口味數據* @param flavors*/void insertBatch(List<DishFlavor> flavors);}
- ????????在/resources/mapper中創建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>
2.3 測試
? ? ? ? 重啟項目,登錄項目進行聯調測試,新增菜品,查看數據庫新增成功