1. 利用AOP實現(xiàn)公共字段的自動填充
1.1 應(yīng)用場景
- 項目中設(shè)計的員工表, 菜品表, 套餐表和分類表中均有涉及到創(chuàng)建時間, 創(chuàng)建人, 更新時間, 更新人這四個字段, 當我們對這四張表進行插入數(shù)據(jù)和修改數(shù)據(jù)時, 就涉及到對這四個字段的設(shè)置. 其中, 當插入數(shù)據(jù)時, 四個字段都需要設(shè)置, 當修改數(shù)據(jù)時, 則只需要設(shè)置更新時間和更新人, 所以對于這些重復的業(yè)務(wù)代碼, 可以采用AOP技術(shù)來實現(xiàn)這些公共字段的自動填充.
1.2 實現(xiàn)步驟
- 自定義注解@AutoFill 以及枚舉類 OperationType
/**
* 自定義注解, 方便某些功能字段的自動填充
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//數(shù)據(jù)庫操作類型: UPDATE INSERT
OperationType value();
}
- 因為插入和更新所需操作的字段不同, 所以創(chuàng)建枚舉類來區(qū)分操作的類型
/**
* 數(shù)據(jù)庫操作類型
*/
public enum OperationType {
/**
* 更新操作
*/
UPDATE,
/**
* 插入操作
*/
INSERT
}
- 創(chuàng)建切面類利用AOP和反射的技術(shù)來實現(xiàn)公共字段自動填充的代碼邏輯
/**
* 自定義切面, 實現(xiàn)公共字段自動填充代碼邏輯
*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 切入點
*/
@Before("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFill(JoinPoint joinPoint) {
log.info("開始進行公共字段的自動填充...");
//獲取當前被攔截的方法上的數(shù)據(jù)庫操作類型
//INSERT則需要更改四個數(shù)據(jù)
//UPDATE則只需要更改兩個數(shù)據(jù)
MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法簽名對象
OperationType value = signature.getMethod().getAnnotation(AutoFill.class).value();//獲得方法注解的數(shù)據(jù)類型
//獲取當前被攔截方法的參數(shù), 約定參數(shù)中的實體類放在第一位
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) return;
//獲取第一位的實體參數(shù), 約定參數(shù)中的實體類放在第一位
//選用Object來接收是因為操作的表不一樣, 傳輸?shù)膶嶓w類不同, 所以直接采用Object
Object entity = args[0];
//準備賦值的數(shù)據(jù)
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
//根據(jù)當前不同的數(shù)據(jù)庫類型, 來通過反射為相關(guān)屬性賦值
if (value == OperationType.INSERT) {
//為4個公共字段賦值
try {
//先得到4個set方法
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) {
throw new RuntimeException(e);
}
} else if (value == OperationType.UPDATE) {
//為2個公共字段賦值
try {
//先得到2個set方法
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) {
throw new RuntimeException(e);
}
}
}
}
2. 使用阿里云來存儲上傳的圖片
2.1 應(yīng)用場景
- 為了實現(xiàn)菜品圖片的上傳功能, 采用阿里云OSS來存儲上傳的圖片, 并返回圖片的url, 使其能夠在前端頁面顯示.
2.2 實現(xiàn)步驟
- 通過配置阿里云OSS相關(guān)配置屬性, 以及圖片上傳工具類來完成圖片上傳.
3. 使用Redis來存儲店鋪的營業(yè)狀態(tài)
3.1 應(yīng)用場景
- 店鋪的營業(yè)狀態(tài)設(shè)置為營業(yè)中和打烊中, 只有兩種狀態(tài), 所以可以將店鋪的營業(yè)狀態(tài)存儲到數(shù)據(jù)庫中, 方便修改和查詢, 由于僅有一個字段, 如果用Mysql數(shù)據(jù)庫來存儲較為浪費, 所以可以采用Redis數(shù)據(jù)庫來存儲.
3.2 實現(xiàn)步驟
- 導入Redis的maven坐標
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 創(chuàng)建redis的配置類
@Configuration
@Slf4j
public class RedisConfiguration {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
log.info("開始創(chuàng)建redis模板對象...");
RedisTemplate redisTemplate = new RedisTemplate();
//設(shè)置redis的連接工廠對象
redisTemplate.setConnectionFactory(redisConnectionFactory);
//設(shè)置redis Key序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
- 注入RedisTemplate 并實現(xiàn)相關(guān)代碼, RedisTemplate 即為reids對象
public static final String KEY = "SHOP_STATUS";
@Autowired
private RedisTemplate redisTemplate;
/**
* 設(shè)置店鋪營業(yè)狀態(tài)
*
* @param status status
* @return @return
*/
@PutMapping("/{status}")
public Result setStatus(@PathVariable Integer status) {
log.info("設(shè)置店鋪營業(yè)狀態(tài)為: {}", status.equals(StatusConstant.ENABLE) ? "營業(yè)中" : "打烊中");
redisTemplate.opsForValue().set(KEY, status);
return Result.success();
}
4. 利用Redis以及Spring Cache來實現(xiàn)菜品套餐數(shù)據(jù)的緩存
4.1 應(yīng)用場景
- 在小程序端用戶點餐時就會頻繁查詢菜品及套餐數(shù)據(jù), 沒有緩存時, 用戶每切換一個分類就會重新查詢一次, 如果多個用戶查詢頻繁, 會使數(shù)據(jù)庫的壓力較大, 所以我們可以將查詢出來的數(shù)據(jù)緩存到Redis當中, 這樣就不會頻繁查詢數(shù)據(jù)庫, 當數(shù)據(jù)發(fā)生變更時, 我們將Redis中的原先的緩存刪除, 再次查詢再添加到緩存中即可.
4.2 Spring Cache介紹
- Spring Cache是一個框架, 實現(xiàn)了基于注解的緩存功能, 只需要簡單地加一個注解, 就能實現(xiàn)緩存功能. Spring Cache提供了一層抽象, 底層可以切換不同的緩存實現(xiàn), 例如: EHCache, Caffeine, Redis . 我們導入不同的坐標即可切換不同的緩存, 無需改動代碼.
- 導入Spring Cache的maven坐標
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
-
Spring Cache常用注解
4.3 實現(xiàn)步驟
- 導入Spring Cache和Redis的maven坐標(已導入)
- 在啟動類上加入@EnableCaching注解, 開啟緩存注解功能
@EnableCaching //開啟緩存注解功能
- 在用戶端接口SetmealController的 list方法上加入@Cacheable注解
@Cacheable(cacheNames = "setmealCache",key = "#categoryId") //cacheNames::key
- 在管理端接口SetmealController的save、delete、update览芳、startOrStop等方法上加入CacheEvict注解(數(shù)據(jù)發(fā)生變更則清理緩存)
@CacheEvict(cacheNames = "setmealCache", key = "setmealDTO.categoryId")//精確清理
@CacheEvict(cacheNames = "setmealCache", allEntries = true)//清除所有
5. 利用Spring Task完成過期訂單的定時處理
5.1 應(yīng)用場景
- 當用戶下單后, 未在15分鐘之內(nèi)支付, 商家應(yīng)該自動取消該訂單. 對于每天商家派送的訂單未及時點擊完成, 我們決定在每天的凌晨1點來自動處理, 將未及時點擊完成的訂單自動更改為完成.
5.2 Spring Task介紹
- Spring Task是一個任務(wù)調(diào)度框架, 用于處理定時任務(wù). 例如: 每月還款提醒, 超時未支付取消訂單, 生日發(fā)送祝福等等
5.2.1 Cron表達式
- 是一個字符串, 可以通過cron表達式定義任務(wù)觸發(fā)的時間
- 構(gòu)成規(guī)則: 分為6個或7個域, 有空格分隔開, 每個域代表一個含義
-
每個域的含義分別為: 秒, 分鐘, 小時, 日, 月, 周, 年(可選)
5.2.2 Cron表達式在線生成器
- 為了方便我們書寫cron表達式, 我們可以采用在線生成器來輔助
在線Cron表達式生成器 (qqe2.com)
5.2.3 Spring Task使用步驟
- 導入maven坐標 spring-context
- 啟動類上添加注解 @EnableScheduling 開啟任務(wù)調(diào)度
@EnableScheduling //開啟任務(wù)調(diào)度
- 自定義定時任務(wù)類(包含定時任務(wù)的業(yè)務(wù)邏輯, 觸發(fā)時間), 類中寫方法來實現(xiàn)業(yè)務(wù)邏輯, 在方法上加 @Scheduled 注解寫入cron表達式來指定觸發(fā)時間
@Component //需要實例化,交給IOC容器管理
@Slf4j
public class MyTask {
/**
* 定時任務(wù),每隔5秒觸發(fā)一次
*/
@Scheduled(cron = "0/5 * * * * ?")
public void executeTask() {
log.info("定時任務(wù)開始執(zhí)行: {}", new Date());
}
}
部分輸出結(jié)果如下
定時任務(wù)開始執(zhí)行: Mon Jul 29 20:06:20 CST 2024
定時任務(wù)開始執(zhí)行: Mon Jul 29 20:06:25 CST 2024
定時任務(wù)開始執(zhí)行: Mon Jul 29 20:06:30 CST 2024
定時任務(wù)開始執(zhí)行: Mon Jul 29 20:06:35 CST 2024
5.3 業(yè)務(wù)代碼實現(xiàn)
/**
* 定時任務(wù)類,定時處理超時狀態(tài)訂單
*/
@Slf4j
@Component
public class OrderTask {
@Autowired
private OrderMapper orderMapper;
/**
* 處理超時訂單的方法
*/
@Scheduled(cron = "0 * * * * ?") //每分鐘觸發(fā)一次
//@Scheduled(cron = "1/5 * * * * ?") //測試采用第一秒開始,每五秒觸發(fā)
public void processTimeoutOrder() {
log.info("定時處理超時訂單: {}", LocalDateTime.now());
//select * from orders where status = ? and order_time < (當前時間 - 15分鐘)
LocalDateTime OrderTime = LocalDateTime.now().plusMinutes(-15);
List<Orders> list = orderMapper.getByStatusAndOrderTime(Orders.PENDING_PAYMENT, OrderTime);
if (list != null && !list.isEmpty()) {
for (Orders orders : list) {
orders.setStatus(Orders.CANCELLED);
orders.setCancelTime(LocalDateTime.now());
orders.setCancelReason(MessageConstant.ORDER_PAID_OVER_TIME);
orderMapper.update(orders);
}
}
}
/**
* 處理一直處于派送中的訂單
*/
@Scheduled(cron = "0 0 1 * * ?") //每天凌晨1點觸發(fā)
//@Scheduled(cron = "0/5 * * * * ?") //測試采用第0秒開始,每五秒觸發(fā)
public void processDeliveryOrder() {
log.info("凌晨1點處理處于派送中的訂單: {}", LocalDateTime.now());
LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
List<Orders> list = orderMapper.getByStatusAndOrderTime(Orders.DELIVERY_IN_PROGRESS, time);
if (list != null && !list.isEmpty()) {
for (Orders orders : list) {
orders.setStatus(Orders.COMPLETED);
orderMapper.update(orders);
}
}
}
}
6. 使用Web Socket完成客戶端與服務(wù)器的長連接
6.1 應(yīng)用場景
- 當用戶下單時, 管理客戶端要實時有來單提醒及語音播報; 當用戶催單時, 也要實時語音播報.
6.2 Web Socket介紹
- WebSocket是基于TCP的一種新的網(wǎng)絡(luò)協(xié)議抱究。它實現(xiàn)了瀏覽器與服務(wù)器全雙工通信——瀏覽器和服務(wù)器只需要完成一次握手蛋哭,兩者之間就可以創(chuàng)建持久性的連接添祸,并進行雙向數(shù)據(jù)傳輸。
6.2.1 與HTTP協(xié)議的對比
6.2.2 Web Socket應(yīng)用場景
- 觀看視頻的彈幕
- 網(wǎng)頁聊天對話
- 體育實況數(shù)據(jù)實時更新
- 股票基金報價實時更新
6.2.3 WebSocket缺點:
- 服務(wù)器長期維護長連接需要一定的成本, 各個瀏覽器支持程度不一樣, WebSocket 是長連接绍申,受網(wǎng)絡(luò)限制比較大拒名,需要處理好重連
結(jié)論:WebSocket并不能完全取代HTTP吩愧,它只適合在特定的場景下使用
7. 利用Apache POI完成報表的制作
7.1 應(yīng)用場景
- 當管理人員點擊數(shù)據(jù)導出時, 可以得到近30天的銷售數(shù)據(jù)統(tǒng)計報表(Excel表).
7.2 Apache POI介紹
- Apache POI 是一個處理 Miscrosoft Office 各種文件格式的開源項目. 簡單來說就是, 我們可以使用 POI 在 Java 程序中對 Miscrosoft Office 各種文件進行讀寫操作, 一般情況下, POI 都是用于操作 Excel 文件.
7.2.1 Apache POI應(yīng)用場景
- 銀行網(wǎng)銀系統(tǒng)導出交易明細
- 各種業(yè)務(wù)系統(tǒng)導出Excel報表
- 批量導入業(yè)務(wù)數(shù)據(jù)
7.2.2 使用POI的步驟
- 導入 Apache POI 的 maven 坐標
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
</dependency>
- 根據(jù)POI提供的各種方法來操作Excel文件(讀取操作需要結(jié)合輸入輸出流來實現(xiàn))
7.3 業(yè)務(wù)代碼
- 對于復雜的表格, 我們一般不采用Java代碼來制作Excel表, 先提前創(chuàng)建好Excel表, 然后導入表格, 以該表格為模板, 利用Java代碼往里面填充數(shù)據(jù)即可.
/**
* 導出運營數(shù)據(jù)報表
*
* @param response response
*/
@Override
public void exportBusinessDate(HttpServletResponse response) {
//1. 查詢數(shù)據(jù)庫,獲取營業(yè)數(shù)據(jù)--查詢最近30天營業(yè)數(shù)據(jù)
LocalDate dateBeginTime = LocalDate.now().minusDays(30);
LocalDate dateEndTime = LocalDate.now().minusDays(1);
LocalDateTime beginTime = LocalDateTime.of(dateBeginTime, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(dateEndTime, LocalTime.MAX);
BusinessDataVO businessData = workspaceService.getBusinessData(beginTime, endTime);
//2. 通過POI將數(shù)據(jù)寫入到Excel文件中
//獲取輸入流將Excel文件模板讀取出來
InputStream inputStream = this.getClass().getClassLoader()
.getResourceAsStream("template/運營數(shù)據(jù)報表模板.xlsx");
try {
//基于模板創(chuàng)建新的Excel文件
assert inputStream != null;
XSSFWorkbook excel = new XSSFWorkbook(inputStream);
//獲取表格文件的sheet頁
XSSFSheet sheet = excel.getSheet("Sheet1");
//填充數(shù)據(jù)時間
sheet.getRow(1).getCell(1).setCellValue("時間" + dateBeginTime + "至" + dateEndTime);
//獲得第4行并填充相關(guān)數(shù)據(jù)
XSSFRow row = sheet.getRow(3);
row.getCell(2).setCellValue(businessData.getTurnover());
row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
row.getCell(6).setCellValue(businessData.getNewUsers());
//獲得第5行并填充相關(guān)數(shù)據(jù)
row = sheet.getRow(4);
row.getCell(2).setCellValue(businessData.getValidOrderCount());
row.getCell(4).setCellValue(businessData.getUnitPrice());
//填充明細
for (int i = 0; i < 30; i++) {
//獲取到某一天
LocalDate date = dateBeginTime.plusDays(i);
//查詢某一天的營業(yè)數(shù)據(jù)
BusinessDataVO vo = workspaceService.getBusinessData(LocalDateTime.of(date, LocalTime.MIN),
LocalDateTime.of(date, LocalTime.MAX));
//獲得某一行
row = sheet.getRow(7 + i);
//填充數(shù)據(jù)
row.getCell(1).setCellValue(date.toString());
row.getCell(2).setCellValue(vo.getTurnover());
row.getCell(3).setCellValue(vo.getValidOrderCount());
row.getCell(4).setCellValue(vo.getOrderCompletionRate());
row.getCell(5).setCellValue(vo.getUnitPrice());
row.getCell(6).setCellValue(vo.getNewUsers());
}
//3. 通過輸出流將Excel文件下載到客戶端瀏覽器
ServletOutputStream outputStream = response.getOutputStream();
excel.write(outputStream);
//關(guān)閉資源
outputStream.close();
excel.close();
inputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}