1. 利用AOP实现公共字段的自动填充
1.1 应用场景
- 项目中设计的员工表, 菜品表, 套餐表和分类表中均有涉及到创建时间, 创建人, 更新时间, 更新人这四个字段, 当我们对这四张表进行插入数据和修改数据时, 就涉及到对这四个字段的设置. 其中, 当插入数据时, 四个字段都需要设置, 当修改数据时, 则只需要设置更新时间和更新人, 所以对于这些重复的业务代码, 可以采用AOP技术来实现这些公共字段的自动填充.
1.2 实现步骤
- 自定义注解@AutoFill 以及枚举类 OperationType
/**
* 自定义注解, 方便某些功能字段的自动填充
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型: UPDATE INSERT
OperationType value();
}
- 因为插入和更新所需操作的字段不同, 所以创建枚举类来区分操作的类型
/**
* 数据库操作类型
*/
public enum OperationType {
/**
* 更新操作
*/
UPDATE,
/**
* 插入操作
*/
INSERT
}
- 创建切面类利用AOP和反射的技术来实现公共字段自动填充的代码逻辑
/**
* 自定义切面, 实现公共字段自动填充代码逻辑
*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 切入点
*/
@Before("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFill(JoinPoint joinPoint) {
log.info("开始进行公共字段的自动填充...");
//获取当前被拦截的方法上的数据库操作类型
//INSERT则需要更改四个数据
//UPDATE则只需要更改两个数据
MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象
OperationType value = signature.getMethod().getAnnotation(AutoFill.class).value();//获得方法注解的数据类型
//获取当前被拦截方法的参数, 约定参数中的实体类放在第一位
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) return;
//获取第一位的实体参数, 约定参数中的实体类放在第一位
//选用Object来接收是因为操作的表不一样, 传输的实体类不同, 所以直接采用Object
Object entity = args[0];
//准备赋值的数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
//根据当前不同的数据库类型, 来通过反射为相关属性赋值
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 应用场景
- 为了实现菜品图片的上传功能, 采用阿里云OSS来存储上传的图片, 并返回图片的url, 使其能够在前端页面显示.
2.2 实现步骤
- 通过配置阿里云OSS相关配置属性, 以及图片上传工具类来完成图片上传.
3. 使用Redis来存储店铺的营业状态
3.1 应用场景
- 店铺的营业状态设置为营业中和打烊中, 只有两种状态, 所以可以将店铺的营业状态存储到数据库中, 方便修改和查询, 由于仅有一个字段, 如果用Mysql数据库来存储较为浪费, 所以可以采用Redis数据库来存储.
3.2 实现步骤
- 导入Redis的maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 创建redis的配置类
@Configuration
@Slf4j
public class RedisConfiguration {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
log.info("开始创建redis模板对象...");
RedisTemplate redisTemplate = new RedisTemplate();
//设置redis的连接工厂对象
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置redis Key序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
- 注入RedisTemplate 并实现相关代码, RedisTemplate 即为reids对象
public static final String KEY = "SHOP_STATUS";
@Autowired
private RedisTemplate redisTemplate;
/**
* 设置店铺营业状态
*
* @param status status
* @return @return
*/
@PutMapping("/{status}")
public Result setStatus(@PathVariable Integer status) {
log.info("设置店铺营业状态为: {}", status.equals(StatusConstant.ENABLE) ? "营业中" : "打烊中");
redisTemplate.opsForValue().set(KEY, status);
return Result.success();
}
4. 利用Redis以及Spring Cache来实现菜品套餐数据的缓存
4.1 应用场景
- 在小程序端用户点餐时就会频繁查询菜品及套餐数据, 没有缓存时, 用户每切换一个分类就会重新查询一次, 如果多个用户查询频繁, 会使数据库的压力较大, 所以我们可以将查询出来的数据缓存到Redis当中, 这样就不会频繁查询数据库, 当数据发生变更时, 我们将Redis中的原先的缓存删除, 再次查询再添加到缓存中即可.
4.2 Spring Cache介绍
- Spring Cache是一个框架, 实现了基于注解的缓存功能, 只需要简单地加一个注解, 就能实现缓存功能. Spring Cache提供了一层抽象, 底层可以切换不同的缓存实现, 例如: EHCache, Caffeine, Redis . 我们导入不同的坐标即可切换不同的缓存, 无需改动代码.
- 导入Spring Cache的maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
-
Spring Cache常用注解
4.3 实现步骤
- 导入Spring Cache和Redis的maven坐标(已导入)
- 在启动类上加入@EnableCaching注解, 开启缓存注解功能
@EnableCaching //开启缓存注解功能
- 在用户端接口SetmealController的 list方法上加入@Cacheable注解
@Cacheable(cacheNames = "setmealCache",key = "#categoryId") //cacheNames::key
- 在管理端接口SetmealController的save、delete、update、startOrStop等方法上加入CacheEvict注解(数据发生变更则清理缓存)
@CacheEvict(cacheNames = "setmealCache", key = "setmealDTO.categoryId")//精确清理
@CacheEvict(cacheNames = "setmealCache", allEntries = true)//清除所有
5. 利用Spring Task完成过期订单的定时处理
5.1 应用场景
- 当用户下单后, 未在15分钟之内支付, 商家应该自动取消该订单. 对于每天商家派送的订单未及时点击完成, 我们决定在每天的凌晨1点来自动处理, 将未及时点击完成的订单自动更改为完成.
5.2 Spring Task介绍
- Spring Task是一个任务调度框架, 用于处理定时任务. 例如: 每月还款提醒, 超时未支付取消订单, 生日发送祝福等等
5.2.1 Cron表达式
- 是一个字符串, 可以通过cron表达式定义任务触发的时间
- 构成规则: 分为6个或7个域, 有空格分隔开, 每个域代表一个含义
-
每个域的含义分别为: 秒, 分钟, 小时, 日, 月, 周, 年(可选)
5.2.2 Cron表达式在线生成器
- 为了方便我们书写cron表达式, 我们可以采用在线生成器来辅助
在线Cron表达式生成器 (qqe2.com)
5.2.3 Spring Task使用步骤
- 导入maven坐标 spring-context
- 启动类上添加注解 @EnableScheduling 开启任务调度
@EnableScheduling //开启任务调度
- 自定义定时任务类(包含定时任务的业务逻辑, 触发时间), 类中写方法来实现业务逻辑, 在方法上加 @Scheduled 注解写入cron表达式来指定触发时间
@Component //需要实例化,交给IOC容器管理
@Slf4j
public class MyTask {
/**
* 定时任务,每隔5秒触发一次
*/
@Scheduled(cron = "0/5 * * * * ?")
public void executeTask() {
log.info("定时任务开始执行: {}", new Date());
}
}
部分输出结果如下
定时任务开始执行: Mon Jul 29 20:06:20 CST 2024
定时任务开始执行: Mon Jul 29 20:06:25 CST 2024
定时任务开始执行: Mon Jul 29 20:06:30 CST 2024
定时任务开始执行: Mon Jul 29 20:06:35 CST 2024
5.3 业务代码实现
/**
* 定时任务类,定时处理超时状态订单
*/
@Slf4j
@Component
public class OrderTask {
@Autowired
private OrderMapper orderMapper;
/**
* 处理超时订单的方法
*/
@Scheduled(cron = "0 * * * * ?") //每分钟触发一次
//@Scheduled(cron = "1/5 * * * * ?") //测试采用第一秒开始,每五秒触发
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点触发
//@Scheduled(cron = "0/5 * * * * ?") //测试采用第0秒开始,每五秒触发
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完成客户端与服务器的长连接
6.1 应用场景
- 当用户下单时, 管理客户端要实时有来单提醒及语音播报; 当用户催单时, 也要实时语音播报.
6.2 Web Socket介绍
- WebSocket是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
6.2.1 与HTTP协议的对比
6.2.2 Web Socket应用场景
- 观看视频的弹幕
- 网页聊天对话
- 体育实况数据实时更新
- 股票基金报价实时更新
6.2.3 WebSocket缺点:
- 服务器长期维护长连接需要一定的成本, 各个浏览器支持程度不一样, WebSocket 是长连接,受网络限制比较大,需要处理好重连
结论:WebSocket并不能完全取代HTTP,它只适合在特定的场景下使用
7. 利用Apache POI完成报表的制作
7.1 应用场景
- 当管理人员点击数据导出时, 可以得到近30天的销售数据统计报表(Excel表).
7.2 Apache POI介绍
- Apache POI 是一个处理 Miscrosoft Office 各种文件格式的开源项目. 简单来说就是, 我们可以使用 POI 在 Java 程序中对 Miscrosoft Office 各种文件进行读写操作, 一般情况下, POI 都是用于操作 Excel 文件.
7.2.1 Apache POI应用场景
- 银行网银系统导出交易明细
- 各种业务系统导出Excel报表
- 批量导入业务数据
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>
- 根据POI提供的各种方法来操作Excel文件(读取操作需要结合输入输出流来实现)
7.3 业务代码
- 对于复杂的表格, 我们一般不采用Java代码来制作Excel表, 先提前创建好Excel表, 然后导入表格, 以该表格为模板, 利用Java代码往里面填充数据即可.
/**
* 导出运营数据报表
*
* @param response response
*/
@Override
public void exportBusinessDate(HttpServletResponse response) {
//1. 查询数据库,获取营业数据--查询最近30天营业数据
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将数据写入到Excel文件中
//获取输入流将Excel文件模板读取出来
InputStream inputStream = this.getClass().getClassLoader()
.getResourceAsStream("template/运营数据报表模板.xlsx");
try {
//基于模板创建新的Excel文件
assert inputStream != null;
XSSFWorkbook excel = new XSSFWorkbook(inputStream);
//获取表格文件的sheet页
XSSFSheet sheet = excel.getSheet("Sheet1");
//填充数据时间
sheet.getRow(1).getCell(1).setCellValue("时间" + dateBeginTime + "至" + dateEndTime);
//获得第4行并填充相关数据
XSSFRow row = sheet.getRow(3);
row.getCell(2).setCellValue(businessData.getTurnover());
row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
row.getCell(6).setCellValue(businessData.getNewUsers());
//获得第5行并填充相关数据
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);
//查询某一天的营业数据
BusinessDataVO vo = workspaceService.getBusinessData(LocalDateTime.of(date, LocalTime.MIN),
LocalDateTime.of(date, LocalTime.MAX));
//获得某一行
row = sheet.getRow(7 + i);
//填充数据
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);
//关闭资源
outputStream.close();
excel.close();
inputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}