《苍穹外卖》项目相关技术总结

1. 利用AOP实现公共字段的自动填充

1.1 应用场景

  • 项目中设计的员工表, 菜品表, 套餐表和分类表中均有涉及到创建时间, 创建人, 更新时间, 更新人这四个字段, 当我们对这四张表进行插入数据和修改数据时, 就涉及到对这四个字段的设置. 其中, 当插入数据时, 四个字段都需要设置, 当修改数据时, 则只需要设置更新时间和更新人, 所以对于这些重复的业务代码, 可以采用AOP技术来实现这些公共字段的自动填充.

1.2 实现步骤

  1. 自定义注解@AutoFill 以及枚举类 OperationType
/**
 * 自定义注解, 方便某些功能字段的自动填充
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
    //数据库操作类型: UPDATE   INSERT
    OperationType value();
}
  • 因为插入和更新所需操作的字段不同, 所以创建枚举类来区分操作的类型
/**
 * 数据库操作类型
 */
public enum OperationType {
    /**
     * 更新操作
     */
    UPDATE,
    /**
     * 插入操作
     */
    INSERT
}
  1. 创建切面类利用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 实现步骤

  1. 导入Redis的maven坐标
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
  1. 创建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;
    }
}
  1. 注入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 实现步骤

  1. 导入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表达式在线生成器

5.2.3 Spring Task使用步骤

  1. 导入maven坐标 spring-context
  2. 启动类上添加注解 @EnableScheduling 开启任务调度
@EnableScheduling //开启任务调度
  1. 自定义定时任务类(包含定时任务的业务逻辑, 触发时间), 类中写方法来实现业务逻辑, 在方法上加 @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的步骤

  1. 导入 Apache POI 的 maven 坐标
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
        </dependency>
  1. 根据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);
        }
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容