前言
用于记录苍穹外卖Day10、Day11、Day12的学习
Day10 订单状态定时处理 来电提醒 客户催单
订单状态定时处理
Spring Task
Spring Task是一个任务调度工具,可以按照约定的时间自动执行某个代码逻辑(定时自动执行某段Java代码)
cron表达式:
cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间。
构成规则:分为6或7个域,用空格隔开,每个域代表一个含义。从左到右依次为秒、分钟、小时、日、月、周、年(可选)
cron在线生成器:https://cron.qqe2.com
Spring Task使用步骤:
- 导入坐标spring-context
- 启动类添加注解@EnableScheduling开启任务调度
- 自定义定时任务类
需求开发
存在的问题:
- 下单后未支付,订单一直处于”待支付“状态
- 用户收货后管理端未点击完成按钮,订单一直处于”派送中“状态
只需自定义个任务处理类来定时处理即可:
//定时任务类,定时处理订单状态
@Component
@Slf4j
public class OrderTask {@Autowiredprivate OrderMapper orderMapper;//处理下单后未支付超时的情况@Scheduled(cron = "0 * * * * ? *")//每分钟触发一次
// @Scheduled(cron="0/5 * * * * ?")public void processTimeOut(){log.info("定时处理下单未支付的订单");//当前时间减15分钟LocalDateTime localDateTime = LocalDateTime.now().plusMinutes(-15);List<Orders> list = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, localDateTime);if(list!=null&&list.size()>0){for (Orders orders : list) {orders.setStatus(Orders.CANCELLED);orders.setConsignee("订单超时,自动取消");orders.setCancelTime(LocalDateTime.now());orderMapper.update(orders);}}}//处理一直处于派送中,没有完成的订单@Scheduled(cron = "0 0 1 * * ?")//每天凌晨一点触发
// @Scheduled(cron="0/10 * * * * ?")public void processDeliveryOrder(){log.info("定时处理一直在派送的订单");LocalDateTime localDateTime = LocalDateTime.now().plusMinutes(-60);List<Orders> list = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, localDateTime);if(list!=null&&list.size()>0){for (Orders orders : list) {orders.setStatus(Orders.COMPLETED);orderMapper.update(orders);}}}
}
来电提醒
WebSocket
WebSocket是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工通信–浏览器和服务器只需完成一次握手,两者之间即可建立持久性的连接,并进行双向数据传输。
WebSocket和HTTP对比:
- HTTP是短连接;WebSocket是长连接
- HTTP是单向通信,基于请求响应模型;WebSocket支持双向通信。
- WebSocket和HTTP都是基于TCP协议
应用场景:
- 视频弹幕
- 实况更新
- 网页聊天
需求开发
实现思路:
- 通过WebSocket实现管理端页面和服务端保持长连接
- 当客户支付后,调用WebSocket的相关API从服务端向客户端推送消息
- 客户端解析服务端发送的消息,判断是来电提醒还是客户催单,并进行相应的语音播报
- 约定服务端向客户端发送的消息的数据格式为JSON,字段包括:type(消息类型,1为来单提醒、2为客户催单)、orderId、content(消息内容)
这里我们只需要在支付成功后提示管理端即可,在OrderServiceImpl的paySuccess方法中:
//通过WebSocket向客户端浏览器推送数据Map map=new HashMap();map.put("type",1);map.put("orderId",ordersDB.getId());map.put("content","订单号:"+outTradeNo);String Json= JSON.toJSONString(map);webSocketServer.sendToAllClient(Json);
注意:启动项目的时候看看你是否连接上WebSocket,如果没连接上可能是因为自己修改过端口号的问题,将端口号改回80或者改下前端代码即可。
客户催单
实现思路和来电提醒差不多。当用户在客户端点击催单按钮时,发起请求
- OrderController
@GetMapping("/reminder/{id}")@ApiOperation("客户催单")public Result reminder(@PathVariable Long id){orderService.reminder(id);return Result.success();}
- OrderServiceImpl
public void reminder(Long id) {// 根据id查询订单Orders orders1= orderMapper.getById(id);// 校验订单是否存在if (orders1 == null) {throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);}//通过WebSocket向客户端浏览器推送数据Map map=new HashMap();map.put("type",2);map.put("orderId",id);map.put("content","订单号:"+orders1.getNumber());String Json= JSON.toJSONString(map);webSocketServer.sendToAllClient(Json);}
Day11 数据统计-图形报表
效果如下所示:
Apache ECharts
Apache ECharts是一款基于JavaScript的数据可视化图表库,提供直观、生动、可交互、可个性化定制的数据可视化图表。简单来说,它就是一款数据可视化工具。我们只需大致知道它是干啥的,它是在前端使用的,后端开发中我们使用不到。
营业额统计
业务规则:
- 营业额指订单状态为已完成的订单金额合计
- 基于可视化报表的折线图展示营业额数据,x轴为日期,y轴为营业额
- 根据时间选择区间,展示每天的营业额数据
ReportController:注意时间的数据格式
@RestController
@Slf4j
@RequestMapping("/admin/report")
@Api(tags = "数据统计相关接口")
public class ReportController {@Autowiredprivate ReportService reportService;@GetMapping("/turnoverStatistics")@ApiOperation("营业额统计")public Result<TurnoverReportVO> turnoverStatistics(@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){log.info("营业额数据统计:{},{}",begin,end);TurnoverReportVO turnoverReportVO=reportService.getTurnoverStatistics(begin,end);return Result.success(turnoverReportVO);}
}
ReportServiceImpl:
这里我的实现方法与课程中略有不同,可以参考一下
public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {TurnoverReportVO turnoverReportVO=new TurnoverReportVO();//1.封装日期数据//先去得到一个日期集合,包含begin到end中的所有日期List<LocalDate> dateList=new ArrayList<>();dateList.add(begin);while(!begin.equals(end)){begin=begin.plusDays(1);dateList.add(begin);}//将集合转成字符串的同时在每个元素间加一个逗号String dateList1=StringUtils.join(dateList,",");turnoverReportVO.setDateList(dateList1);//2.封装营业额数据//查询对应日期的订单的总营业额List<Double> moneyList=new ArrayList<>();for (LocalDate localDate : dateList) {//根据日期查询状态为已完成的订单的营业额//00:00:00LocalDateTime beginTime=LocalDateTime.of(localDate, LocalTime.MIN);//23:59:59LocalDateTime endTime=LocalDateTime.of(localDate, LocalTime.MAX);Map map=new HashMap();map.put("begin",beginTime);map.put("end",endTime);map.put("status", Orders.COMPLETED);Double money=orderMapper.getSumByMap(map);if(money==null){money=0.0;}moneyList.add(money);}String moneyList1=StringUtils.join(moneyList,",");turnoverReportVO.setTurnoverList(moneyList1);return turnoverReportVO;}
用户统计
ReportController:
@GetMapping("/userStatistics")@ApiOperation("用户统计")public Result<UserReportVO> userStatistics(@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){log.info("用户统计:{},{}",begin,end);UserReportVO userReportVO=reportService.getUserStatistics(begin,end);return Result.success(userReportVO);}
ReportServiceImpl:
//用户数据统计public UserReportVO getUserStatistics(LocalDate begin, LocalDate end) {UserReportVO userReportVO=new UserReportVO();//1.封装日期数据//先去得到一个日期集合,包含begin到end中的所有日期List<LocalDate> dateList=new ArrayList<>();dateList.add(begin);while(!begin.equals(end)){begin=begin.plusDays(1);dateList.add(begin);}//将集合转成字符串的同时在每个元素间加一个逗号String dateList1=StringUtils.join(dateList,",");userReportVO.setDateList(dateList1);//2.封装用户总量数据List<Integer> totalList=new ArrayList<>();//3.封装新增用户数量数据List<Integer> newList=new ArrayList<>();for (LocalDate localDate : dateList) {//查询用户表createTime这一天的用户的总量//00:00:00LocalDateTime beginTime=LocalDateTime.of(localDate, LocalTime.MIN);//23:59:59LocalDateTime endTime=LocalDateTime.of(localDate, LocalTime.MAX);Map map=new HashMap();map.put("end",endTime);Integer total=userMapper.countByMap(map);if(total==null){total=0;}totalList.add(total);map.put("begin",beginTime);Integer today= userMapper.countByMap(map);if(today==null){today=0;}newList.add(today);}String userList1=StringUtils.join(totalList,",");userReportVO.setTotalUserList(userList1);String list1=StringUtils.join(newList,",");userReportVO.setNewUserList(list1);return userReportVO;}
订单统计
业务规则:
- 两条折线,一条代表总订单数,另一条代表有效订单数(状态为已完成的订单)
- 展示订单总数、有效订单数、订单完成率数据
ReportController:
@GetMapping("/ordersStatistics")@ApiOperation("订单统计")public Result<OrderReportVO> orderStatistics(@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){log.info("订单统计:{},{}",begin,end);OrderReportVO orderReportVO=reportService.getOrderReportStatistics(begin,end);return Result.success(orderReportVO);}
ReportServiceImpl:
@Overridepublic OrderReportVO getOrderReportStatistics(LocalDate begin, LocalDate end) {OrderReportVO orderReportVO=new OrderReportVO();//1.封装日期数据List<LocalDate> dateList=new ArrayList<>();dateList.add(begin);while(!begin.equals(end)){begin=begin.plusDays(1);dateList.add(begin);}//将集合转成字符串的同时在每个元素间加一个逗号String dateList1=StringUtils.join(dateList,",");orderReportVO.setDateList(dateList1);//2.订单总数List<Integer> totalOrder=new ArrayList<>();//3.有效订单数List<Integer> realOrder=new ArrayList<>();//每天的订单总数以及有效订单数for (LocalDate localDate : dateList) {//00:00:00LocalDateTime beginTime=LocalDateTime.of(localDate, LocalTime.MIN);//23:59:59LocalDateTime endTime=LocalDateTime.of(localDate, LocalTime.MAX);Map map=new HashMap();map.put("begin",beginTime);map.put("end",endTime);Integer total=orderMapper.getByMap(map);if(total==null){total=0;}totalOrder.add(total);map.put("status",Orders.COMPLETED);Integer real=orderMapper.getByMap(map);if(real==null){real=0;}realOrder.add(real);}String totalOrder1=StringUtils.join(totalOrder,",");String realOrder1=StringUtils.join(realOrder,",");//计算时间区间内的订单总数量Integer sum=0;for (Integer integer : totalOrder) {sum+=integer;}//计算时间区间内的有效订单数量Integer real=0;for (Integer integer : realOrder) {real+=integer;}//计算订单完成率double orderCompletionRate=0.0;if (sum!=0) {orderCompletionRate= (double) real /sum;}orderReportVO.setOrderCompletionRate(orderCompletionRate);orderReportVO.setOrderCountList(totalOrder1);orderReportVO.setValidOrderCountList(realOrder1);orderReportVO.setTotalOrderCount(sum);orderReportVO.setValidOrderCount(real);System.out.println(orderReportVO);return orderReportVO;}
销量排名统计
ReportController:
@GetMapping("/top10")@ApiOperation("销量排名top10")public Result<SalesTop10ReportVO> top10(@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){log.info("销量排名top10:{},{}",begin,end);SalesTop10ReportVO salesTop10ReportVO=reportService.getTop10(begin,end);return Result.success(salesTop10ReportVO);}
ReportServiceImpl:
public SalesTop10ReportVO getTop10(LocalDate begin, LocalDate end) {SalesTop10ReportVO salesTop10ReportVO=new SalesTop10ReportVO();//00:00:00LocalDateTime beginTime=LocalDateTime.of(begin, LocalTime.MIN);//23:59:59LocalDateTime endTime=LocalDateTime.of(end, LocalTime.MAX);List<GoodsSalesDTO> goodsSalesDTOList=orderMapper.getSalesTop10(beginTime,endTime);//遍历取出DTO中的numa和number放到对应的集合中去List<String> nameList=new ArrayList<>();List<String> numberList=new ArrayList<>();for (GoodsSalesDTO goodsSalesDTO : goodsSalesDTOList) {nameList.add(goodsSalesDTO.getName());numberList.add(String.valueOf(goodsSalesDTO.getNumber()));}String nameLists=StringUtils.join(nameList,",");String numberLists=StringUtils.join(numberList,",");salesTop10ReportVO.setNameList(nameLists);salesTop10ReportVO.setNumberList(numberLists);return salesTop10ReportVO;}
OrderMapper.xml:
分析一下这里的SQL语句,因为我们要根据订单状态(对应orders表中的status)查订单的名称以及数量(对应order_details表中的number),所以涉及到联表查询。查询前10,所以只需limit 0,10
<!--统计销量前10--><select id="getSalesTop10" resultType="com.sky.dto.GoodsSalesDTO">select od.name,sum(od.number) number from order_detail od,orders o where od.id=o.id and o.status=5<if test="begin!=null">and order_time > #{begin}</if><if test="end!=null">and order_time < #{end}</if>group by od.name order by number desc limit 0,10</select>
Day12 数据统计-Excel报表
工作台
这里课程中的代码是直接导入的,我还是选择手敲一遍。
工作台展示的数据:
- 今日数据
- 订单管理
- 菜品总览
- 套餐总览
- 订单信息
为了简便展示,这里我直接给出一个类中的全部代码了,可以根据注释理解。
- WorkSpaceController
@RestController
@RequestMapping("/admin/workspace")
@Slf4j
@Api(tags = "工作台相关接口")
public class WorkSpaceController {@Autowiredprivate WorkSpaceService workSpaceService;//查询工作台今日数据@GetMapping("/businessData")@ApiOperation("查询工作台今日数据")public Result<BusinessDataVO> businessData(){log.info("查询工作台今日数据");//获取开始时间LocalDateTime beginTime=LocalDateTime.now().with(LocalTime.MIN);//获取结束时间LocalDateTime endTime=LocalDateTime.now().with(LocalTime.MAX);BusinessDataVO businessDataVO=workSpaceService.getBusinessData(beginTime,endTime);return Result.success(businessDataVO);}//查询订单管理数据@GetMapping("/overviewOrders")@ApiOperation("查询订单管理数据")public Result<OrderOverViewVO> overViewOrders(){log.info("查询订单管理数据");return Result.success(workSpaceService.getOrderOverView());}//查询菜品总览@GetMapping("/overviewDishes")@ApiOperation("查询菜品总览")public Result<DishOverViewVO> overViewDishes(){return Result.success(workSpaceService.getDishOverView());}//查询套餐总览@GetMapping("/overviewSetmeals")@ApiOperation("查询套餐总览")public Result<SetmealOverViewVO> overViewSetmeal(){return Result.success(workSpaceService.getSetmealOvermeal());}
}
- WorkSpaceService
public interface WorkSpaceService {BusinessDataVO getBusinessData(LocalDateTime beginTime, LocalDateTime endTime);OrderOverViewVO getOrderOverView();DishOverViewVO getDishOverView();SetmealOverViewVO getSetmealOvermeal();
}
- WorkSpaceServiceImpl(这里很多方法我们都在OrderMapper中已经写好了,直接调用即可)
@Service
public class WorkSpaceServiceImpl implements WorkSpaceService {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate UserMapper userMapper;@Autowiredprivate DishMapper dishMapper;@Autowiredprivate SetmealMapper setmealMapper;//查询工作台今日数据public BusinessDataVO getBusinessData(LocalDateTime beginTime, LocalDateTime endTime) {BusinessDataVO businessDataVO=new BusinessDataVO();Map map=new HashMap();map.put("begin",beginTime);map.put("end",endTime);//订单总数Integer total=orderMapper.getByMap(map);map.put("status",5);//营业额Double sum = orderMapper.getSumByMap(map);sum = sum == null? 0.0 : sum;//有效订单数Integer real=orderMapper.getByMap(map);//平均客单价double average=0.0;//订单完成率double complete=0.0;if(total!=0&&real!=0){complete= (double) real /total;average=sum/real;}//新增用户数Integer newUser=userMapper.countByMap(map);businessDataVO.setTurnover(sum);businessDataVO.setNewUsers(newUser);businessDataVO.setOrderCompletionRate(complete);businessDataVO.setValidOrderCount(real);businessDataVO.setUnitPrice(average);return businessDataVO;}//查询订单管理数据@Overridepublic OrderOverViewVO getOrderOverView() {OrderOverViewVO orderOverViewVO=new OrderOverViewVO();Map map = new HashMap();map.put("begin", LocalDateTime.now().with(LocalTime.MIN));//待接单map.put("status", Orders.TO_BE_CONFIRMED);Integer status1=orderMapper.getByMap(map);//待派送map.put("status",Orders.CONFIRMED);Integer status2=orderMapper.getByMap(map);//已完成map.put("status",Orders.COMPLETED);Integer status3=orderMapper.getByMap(map);//已取消map.put("status",Orders.CANCELLED);Integer status4=orderMapper.getByMap(map);//全部订单map.put("status",null);Integer status5=orderMapper.getByMap(map);orderOverViewVO.setWaitingOrders(status1);orderOverViewVO.setDeliveredOrders(status2);orderOverViewVO.setCompletedOrders(status3);orderOverViewVO.setCancelledOrders(status4);orderOverViewVO.setAllOrders(status5);return orderOverViewVO;}//查询菜品总览@Overridepublic DishOverViewVO getDishOverView() {DishOverViewVO dishOverViewVO=new DishOverViewVO();Integer on=dishMapper.onStatus();Integer off=dishMapper.offStatus();dishOverViewVO.setSold(on);dishOverViewVO.setDiscontinued(off);return dishOverViewVO;}//查询套餐总览@Overridepublic SetmealOverViewVO getSetmealOvermeal() {SetmealOverViewVO setmealOverViewVO=new SetmealOverViewVO();Integer on=setmealMapper.onStatus();Integer off=setmealMapper.offStatus();setmealOverViewVO.setSold(on);setmealOverViewVO.setDiscontinued(off);return setmealOverViewVO;}
}
- DishMapper(这里是SQL语句少我使用这种方法,标准的应该是使用动态SQL)
@Select("select count(id) from dish where status=1")Integer onStatus();@Select("select count(id) from dish where status=0")Integer offStatus();
- SetmealMapper
@Select("select count(id) from setmeal where status=1")Integer onStatus();@Select("select count(id) from setmeal where status=0")Integer offStatus();
Apache POI
简介:Apache POI可以处理Office的各种文件格式。允许我们使用POI在Java程序中对Office文件进行读写操作。一般用于处理Excel文件。
实例:
写入Excel文件:
//在内存中创建一个excel文件XSSFWorkbook excel=new XSSFWorkbook();//在excel文件中创建一个sheet页同时指定其名称为infoXSSFSheet sheet=excel.createSheet("info");//创建行对象,行和列都从0开始,这里我们指定1表示是第二行XSSFRow row= sheet.createRow(1);//创建单元格并写入内容row.createCell(1).setCellValue("姓名");row.createCell(2).setCellValue("爱好");row= sheet.createRow(2);//创建单元格并写入内容row.createCell(1).setCellValue("张三");row.createCell(2).setCellValue("篮球");row= sheet.createRow(3);//创建单元格并写入内容row.createCell(1).setCellValue("李四");row.createCell(2).setCellValue("游泳");//通过输出流将内存中的Excel文件写入到磁盘中FileOutputStream out=new FileOutputStream(new File("E:\\Takeout\\info.xlsx"));excel.write(out);out.close();excel.close();
读取Excel文件:
InputStream in=new FileInputStream(new File("E:\\Takeout\\info.xlsx"));//读取磁盘上已经存在的Excel文件XSSFWorkbook excel=new XSSFWorkbook(in);//读取Excel文件中第一个Sheet页XSSFSheet sheet= excel.getSheetAt(0);//获取Sheet页中最后一行的的行号(有内容的最后一行)int lastRowNum=sheet.getLastRowNum();for (int i = 1; i <= lastRowNum; i++) {//获取某一行XSSFRow row= sheet.getRow(i);//获取单元格对象String stringCellValue1 = row.getCell(1).getStringCellValue();String stringCellValue2 = row.getCell(2).getStringCellValue();System.out.println(stringCellValue1+" "+stringCellValue2);}excel.close();in.close();
导出Excel报表
业务规则:
- 导出Excel文件形式的报表文件
- 导出进30天的运营数据
实现步骤:
- 设计Excel模板文件
- 查询近30日的运营数据
- 将查询到的运营数据写入模板文件内
- 通过输出流将Excel文件下载到客户端浏览器
实现:
- ReportController
@GetMapping("/export")@ApiOperation("导出Excel报表")public Result export(HttpServletResponse response){log.info("导出Excel报表");reportService.export(response);return Result.success();}
- ReportServiceImpl
//导出运营数据报表@Overridepublic void export(HttpServletResponse response) {//1.查询数据库,获取近30日的营业数据LocalDate dateBegin=LocalDate.now().minusDays(30);LocalDate dateEnd=LocalDate.now().minusDays(1);//查询概览数据BusinessDataVO businessDataVO=workSpaceService.getBusinessData(LocalDateTime.of(dateBegin,LocalTime.MIN),LocalDateTime.of(dateEnd,LocalTime.MAX));//2.通过POI将数据写入Excel文件中InputStream in=this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");try{//基于模板文件创建一个新的Excel文件XSSFWorkbook excel=new XSSFWorkbook(in);//获取报表文件的Sheet页XSSFSheet sheet= excel.getSheet("Sheet1");//填充概览数据-时间sheet.getRow(1).getCell(1).setCellValue("时间:"+dateBegin+"到"+dateEnd);//填充概览数据其他数据//第四行XSSFRow row= sheet.getRow(3);row.getCell(2).setCellValue(businessDataVO.getTurnover());row.getCell(4).setCellValue(businessDataVO.getOrderCompletionRate());row.getCell(6).setCellValue(businessDataVO.getNewUsers());//第五行row= sheet.getRow(4);row.getCell(2).setCellValue(businessDataVO.getValidOrderCount());row.getCell(4).setCellValue(businessDataVO.getUnitPrice());for (int i=0;i<30;i++){LocalDate date=dateBegin.plusDays(i);//查询莫一天的数据BusinessDataVO businessDataVO1=workSpaceService.getBusinessData(LocalDateTime.of(date,LocalTime.MIN),LocalDateTime.of(date,LocalTime.MAX));//获取某一行并填充数据row= sheet.getRow(7+i);row.getCell(1).setCellValue(businessDataVO1.toString());row.getCell(2).setCellValue(businessDataVO1.getTurnover());row.getCell(3).setCellValue(businessDataVO1.getValidOrderCount());row.getCell(4).setCellValue(businessDataVO1.getOrderCompletionRate());row.getCell(5).setCellValue(businessDataVO1.getUnitPrice());row.getCell(6).setCellValue(businessDataVO1.getNewUsers());}//3.通过输出流将Excel文件下载到客户端浏览器ServletOutputStream out= response.getOutputStream();excel.write(out);//关闭资源out.close();excel.close();}catch (Exception e){e.printStackTrace();}
这里我们需要注意的一个地方,这里老师没有加上这个后缀,不加的话我这里会报错:
苍穹外卖的学习就到这里啦,完结撒花!!!