订单统计与导入导出

经营看板

本项目在运营端工作台页面展示系统经营看板,内容包括订单分析、用户分析等,如下图:

经营看板是一种用于实时监控关键业务指标的工具,它不仅帮助团队保持敏捷、透明和高效,还促进了团队的协作和创新。

 

需求分析

下边梳理本项目运营端经营看板的功能。

首先选择一个时间区间(不能大于365天),统计在此时间区间内的订单数据,只统计已取消、已关闭、已完成的订单数据。

 

订单分析内容如下:

  • 有效订单数:在统计时间内,订单状态为已完成的订单数。(订单被取消、退款、关闭状态均不属于有效订单)

  • 取消订单数:在统计时间内,提交订单后被取消订总量

  • 关闭订单数:在统计时间内,提交订单后被关闭订总量

  • 有效订单总额:在统计时间内,有效订单的总订单交易总额

  • 实付单均价:在统计时间内,平均每单实际支付额(不包含失效订单)

实付单均价=有效订单总额/有效订单数

订单趋势:

订单趋势的显示分两种情况:

  1. 如果统计时间区间 大于1天,订单趋势显示时间区间内每天的下单总数。

  2. 如果统计时间区间小于等于1天,订单趋势显示当天每小时的下单总数。

技术方案

根据需求,我们要统计一个时间区间的订单总数、订单均价等指标。统计出结果后通过接口将数据返回给前端,前端在界面展示即可。

基于什么平台进行统计分析?

通常统计分析要借助大数据平台进行,流程如下:

 

说明:

大数据统计系统对数据进行统计,并统计结果存入MySQL。

Java程序根据看板的需求提供查询接口,从统计结果表查询数据。这里使用缓存,将看板需要的数据存入Redis,提高查询性能。

如果数据量不大在千万级别可以基于数据库进行统计。

本项目通过分布式数据库存储历史订单数据,可以满足统计分析的需求。

本项目对订单的统计基于数据库进行统计。

如何基于数据库进行统计呢?

当用户进入看板页面面向全部数据 进行实时统计其统计速度较慢。

为了提高统计效率可以分层次聚合,再基于分层聚合的统计结果进行二次统计。

举例:

我们要统计2023年10月1日 到2023年11月30日的订单总数等指标,我们可以提前按天把每天的订单总数等指标统计出来,当用户去统计2023年10月1日 到2023年11月30日的订单总数时基于按天统计的结果进行二次统计。

按天统计结果:

统计数据的分层次聚合需要根据需求确定统计的维度,例如除了按照时间,还可能按地区、产品类别等进行聚合。

根据需求在订单趋势图上除了显示每天的订单总数以外还会按小时进行显示,所以还需要按小时进行统计。

本项目采用滚动式统计,每次统计近15天的数据(如果数据量大可减少统计时段长度),采用滚动式统计的好处是防止统计任务执行失败漏掉统计数据,如下图:

15日统计1到15日的订单。

16日统计2到16日的订单。

依此类推。

分层聚合的粒度有两种:

按天统计,将统计结果存储至按天统计表。

按小时,将统计结果存储至按小时统计表。

有了分层聚合的统计结果,根据用户需求基于分层聚合的统计结果进行二次统计,其统计效率会大大提高,并且有此需求无需进行二次统计直接查询分层聚合结果表即可。

数据流如下:

订单统计

定义mapper

首先编写SQL:

这里面的SQL是行变列,并把数据封装到一个list的实体类中

<?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.jzo2o.orders.history.mapper.HistoryOrdersSyncMapper">

    <select id="statForDay" parameterType="java.util.Map" resultType="com.jzo2o.orders.history.model.domain.StatDay">
        select day as id,
        day as statTime,
        sum(if(orders_status=500,1,0)) effective_order_num,
        sum(if(orders_status=600,1,0)) cancel_order_num,
        sum(if(orders_status=700,1,0)) close_order_num,
        sum(if(orders_status=500,total_amount,0)) effective_order_total_amount
        from history_orders_sync where day >= #{queryDay}
        GROUP BY day
    </select>
    ...

 只统计一部分,另外一部分用流的形式统计

package com.jzo2o.orders.history.service.impl;

@Service
@Slf4j
public class HistoryOrdersSyncServiceImpl extends ServiceImpl<HistoryOrdersSyncMapper, HistoryOrdersSync> implements IHistoryOrdersSyncService {

@Override
public List<StatDay> statForDay(Integer statDay) {
    //统计15天以内的订单
    List<StatDay> statForDay = baseMapper.statForDay(statDay);
    if(CollUtils.isEmpty(statForDay)) {
        return Collections.emptyList();
    }
    // 按天统计订单,计算订单总数、均价等信息
    List<StatDay> collect = statForDay.stream().peek(sd -> {
        // 订单总数
        sd.setTotalOrderNum(NumberUtils.add(sd.getEffectiveOrderNum(), sd.getCloseOrderNum(), sd.getCancelOrderNum()).intValue());
        // 实付订单均价
        if (sd.getEffectiveOrderNum().compareTo(0) == 0) {
            sd.setRealPayAveragePrice(BigDecimal.ZERO);
        } else {
           //RoundingMode.HALF_DOWN 表示四舍五入 向下舍弃,如2.345,保留两位小数为2.34
            BigDecimal realPayAveragePrice = sd.getEffectiveOrderTotalAmount().divide(new BigDecimal(sd.getEffectiveOrderNum()), 2, RoundingMode.HALF_DOWN);
            sd.setRealPayAveragePrice(realPayAveragePrice);
        }
    }).collect(Collectors.toList());

    return collect;
}
...

 定时任务

@Service
public class StatDayServiceImpl extends ServiceImpl<StatDayMapper, StatDay> implements IStatDayService {
@Override
public void statAndSaveData() {
    // 1.数据统计
    // 15天前时间
    LocalDateTime statDayLocalDateTime = DateUtils.now().minusDays(15);
    long statDayTime = DateUtils.getFormatDate(statDayLocalDateTime, "yyyMMdd");
    // 统计数据
    List<StatDay> statDays = historyOrdersSyncService.statForDay((int) statDayTime);
    if(ObjectUtils.isEmpty(statDays)){
        return ;
    }    
    // 2.数据保存至按天统计表
    saveOrUpdateBatch(statDays);
}

 

@Component
public class XxlJobHandler {

    /**
     * 按天统计保存15天内的订单数据
     * 按小时统计保存15天内的订单数据
     */
    @XxlJob("statAndSaveData")
    public void statAndSaveDataForDay() {
        //按天统计保存15天内的订单数据
        statDayService.statAndSaveData();
        //按小时统计保存15天内的订单数据
        statHourService.statAndSaveData();
    }

 

    @GetMapping("/homePage")
    @ApiOperation("运营端首页数据")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "minTime", value = "开始时间", required = true, dataTypeClass = LocalDateTime.class),
            @ApiImplicitParam(name = "maxTime", value = "结束时间", required = true, dataTypeClass = LocalDateTime.class)
    })
    public OperationHomePageResDTO homePage(@RequestParam("minTime") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime minTime,
                                            @RequestParam("maxTime") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime maxTime) {
        return ordersStatisticsService.homePage(minTime, maxTime);
    }
@Cacheable(value = "JZ_CACHE", cacheManager = "cacheManager30Minutes")
    public OperationHomePageResDTO homePage(LocalDateTime minTime, LocalDateTime maxTime) {
        //校验查询时间
        if (LocalDateTimeUtil.between(minTime, maxTime, ChronoUnit.DAYS) > 365) {
            throw new ForbiddenOperationException("查询时间区间不能超过一年");
        }

        //如果查询日期是同一天,则按小时查询折线图数据
        if (LocalDateTimeUtil.beginOfDay(maxTime).equals(minTime)) {
            return getHourOrdersStatistics(minTime);
        } else {
            //如果查询日期不是同一天,则按日查询折线图数据
            return getDayOrdersStatistics(minTime, maxTime);
        }
    }

 

private OperationHomePageResDTO getDayOrdersStatistics(LocalDateTime minTime, LocalDateTime maxTime) {
        //定义要返回的对象
        OperationHomePageResDTO operationHomePageResDTO = OperationHomePageResDTO.defaultInstance();

        //日期格式化,格式:yyyyMMdd
        String minTimeDayStr = LocalDateTimeUtil.format(minTime, DatePattern.PURE_DATE_PATTERN);
        String maxTimeDayStr = LocalDateTimeUtil.format(maxTime, DatePattern.PURE_DATE_PATTERN);

        //根据日期区间聚合统计数据
        StatDay statDay = statDayService.aggregationByIdRange(Long.valueOf(minTimeDayStr), Long.valueOf(maxTimeDayStr));

        //将statDay拷贝到operationHomePageResDTO
        operationHomePageResDTO = BeanUtils.copyIgnoreNull(BeanUtil.toBean(statDay, OperationHomePageResDTO.class), operationHomePageResDTO, OperationHomePageResDTO.class);

        //根据日期区间查询按日统计数据
        List<StatDay> statDayList = statDayService.queryListByIdRange(Long.valueOf(minTimeDayStr), Long.valueOf(maxTimeDayStr));
        //将statDayList转为map<趋势图横坐标,订单总数>
        Map<String, Integer> ordersCountMap = statDayList.stream().collect(Collectors.toMap(s -> dateFormatter(s.getId()), StatDay::getTotalOrderNum));
        //趋势图上全部点
        List<OperationHomePageResDTO.OrdersCount> ordersCountsDef = OperationHomePageResDTO.defaultDayOrdersTrend(minTime, maxTime);
        //遍历ordersCountsDef,将统计出来的ordersCountMap覆盖ordersCountsDef中的数据
        ordersCountsDef.stream().forEach(v->{
            if (ObjectUtil.isNotEmpty(ordersCountMap.get(v.getDateTime()))) {
                v.setCount(ordersCountMap.get(v.getDateTime()));
            }
        });
        //将ordersCountsDef放入operationHomePageResDTO
        operationHomePageResDTO.setOrdersTrend(ordersCountsDef);

        return operationHomePageResDTO;
    }

 订单导出

web上传、下载

 

@Controller
@RequestMapping("/excel/test")
public class ExcelController {

/**
     * 文件下载(失败了会返回一个有部分数据的Excel)
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link DownloadData}
     * <p>
     * 2. 设置返回的 参数
     * <p>
     * 3. 直接写,这里注意,finish的时候会自动关闭OutputStream
     */
    @GetMapping("download")
    public void download(HttpServletResponse response) throws IOException {
        // 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
        String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
        EasyExcel.write(response.getOutputStream(), DemoData.class).sheet("模板").doWrite(data());
       //设置返回body无需包装标识
       response.setHeader(BODY_PROCESSED, "1");                
    }

    /**
     * 文件上传
     * <p>1. 创建excel对应的实体对象 参照{@link UploadData}
     * <p>2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link UploadDataListener}
     * <p>3. 直接读即可
     */
    @PostMapping("upload")
    @ResponseBody
    public String upload(MultipartFile file) throws IOException {
        EasyExcel.read(file.getInputStream(), DemoData.class, new DemoDataListener()).sheet().doRead();
        return "success";
    }
@Api(tags = "运营端 - 订单统计相关接口")
@RestController("operationOrdersStatisticsController")
@RequestMapping("/operation/orders-statistics")
public class OrdersStatisticsController {
    @Resource
    private OrdersStatisticsService ordersStatisticsService;


    /**
     * 文件下载并且失败的时候返回json(默认失败了会返回一个有部分数据的Excel)
     *
     * @since 2.1.1
     */
    @GetMapping("downloadStatistics")
    @ApiOperation("导出统计数据")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "minTime", value = "开始时间", required = true, dataTypeClass = LocalDateTime.class),
            @ApiImplicitParam(name = "maxTime", value = "结束时间", required = true, dataTypeClass = LocalDateTime.class)
    })
    public void downloadStatistics(@RequestParam("minTime") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime minTime,
                                   @RequestParam("maxTime") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime maxTime) throws IOException {
        ordersStatisticsService.downloadStatistics(minTime, maxTime);
    }
}

 

/**
 * 导出统计数据
 *
 * @param minTime 开始时间
 * @param maxTime 结束时间
 */
@Override
public void downloadStatistics(LocalDateTime minTime, LocalDateTime maxTime) throws IOException {
    //校验查询时间
    if (LocalDateTimeUtil.between(minTime, maxTime, ChronoUnit.DAYS) > 365) {
        throw new ForbiddenOperationException("查询时间区间不能超过一年");
    }

    HttpServletResponse response = ResponseUtils.getResponse();
    response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
    response.setCharacterEncoding("utf-8");
    //设置返回body无需包装标识
    response.setHeader(BODY_PROCESSED, "1");
    try {
        //如果查询日期是同一天,则按小时导出数据
        if (LocalDateTimeUtil.beginOfDay(maxTime).equals(minTime)) {
            downloadHourStatisticsData(response, minTime);
        } else {
            //如果查询日期不是同一天,则按日导出数据
            downloadDayStatisticsData(response, minTime, maxTime);
        }

    } catch (Exception e) {
        // 重置response,默认失败了会返回一个有部分数据的Excel
        response.reset();
        response.setContentType("application/json");
        Map<String, String> map = MapUtils.newHashMap();
        map.put("status", "failure");
        map.put("message", "下载文件失败" + e.getMessage());
        response.getWriter().println(JSON.toJSONString(map));
    }
}

private void downloadDayStatisticsData(HttpServletResponse response, LocalDateTime minTime, LocalDateTime maxTime) throws IOException {
        //模板文件路径
        String templateFileName = "static/day_statistics_template.xlsx";

        //转换时间格式,拼接下载文件名称
        String fileNameMinTimeStr = LocalDateTimeUtil.format(minTime, DatePattern.NORM_DATE_PATTERN);
        String fileNameMaxTimeStr = LocalDateTimeUtil.format(maxTime, DatePattern.NORM_DATE_PATTERN);
        String fileName = fileNameMinTimeStr + "~" + fileNameMaxTimeStr + " 全国经营分析统计.xlsx";
        // 这里URLEncoder.encode可以防止中文乱码
        fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName);
        String currentTime = LocalDateTimeUtil.format(LocalDateTime.now(), "yyyy/MM/dd HH:mm:ss");

        //根据id区间查询按天统计数据,并转为map,key为日期,value为日期对应统计数据
        String minTimeDayStr = LocalDateTimeUtil.format(minTime, DatePattern.PURE_DATE_PATTERN);
        String maxTimeDayStr = LocalDateTimeUtil.format(maxTime, DatePattern.PURE_DATE_PATTERN);
        //查询按天统计表
        List<StatDay> statDayList = statDayService.queryListByIdRange(Long.valueOf(minTimeDayStr), Long.valueOf(maxTimeDayStr));
        //转成List<StatisticsData>
        List<StatisticsData> statisticsDataList = BeanUtils.copyToList(statDayList, StatisticsData.class);

        //按月份切分统计数据, 有几个月list中就有几条记录
        List<ExcelMonthData> excelMonthDataList = cutDataListByMonth(statisticsDataList);

        // 生成CellWriteHandler对象,在向单元格写数据时会调用它的afterCellDispose方法
        //  getSpecialHandleDataInfo()方法找到需要格式化处理的行索引,返回CellWriteHandler对象
        EasyExcelUtil easyExcelUtil = getSpecialHandleDataInfo(excelMonthDataList);

        try (ExcelWriter excelWriter = EasyExcel
                //注意,服务器上以jar包运行,只能使用下面第2种方式,第1种方式只能在本地运行成功
//                .write(fileName, StatisticsData.class)
                .write(response.getOutputStream(), StatisticsData.class)
                //注意,服务器上以jar包运行,只能使用下面第3种方式,前2种方式只能在本地运行成功
//                .withTemplate(templateFileName)
//                .withTemplate(FileUtil.getInputStream(templateFileName))
                .withTemplate(FileUtil.class.getClassLoader().getResourceAsStream(templateFileName))
                .autoCloseStream(Boolean.FALSE)
                .registerWriteHandler(easyExcelUtil).build()) {
            // 按天统计,选择第1个sheet,把sheet设置为不需要头
            WriteSheet writeSheet = EasyExcel.writerSheet(0).needHead(Boolean.FALSE).build();

            //构建填充数据,map的key对应模板文件中的{}中的名称
            Map<String, Object> map = MapUtils.newHashMap();
            map.put("startTime", fileNameMinTimeStr);
            map.put("endTime", fileNameMaxTimeStr);
            map.put("currentTime", currentTime);


            //写入填充数据
            excelWriter.fill(map, writeSheet);
            //向单元格式依次写入数据
            for (ExcelMonthData excelMonthData : excelMonthDataList) {
                MonthElement monthElement = new MonthElement(excelMonthData.getMonth());
                excelWriter.write(List.of(monthElement), writeSheet);//月份
                excelWriter.write(excelMonthData.getStatisticsDataList(), writeSheet);//每天的数据
                excelWriter.write(List.of(excelMonthData.getMonthAggregation()), writeSheet);//该天的汇总数据
            }

            excelWriter.finish();
        }
    }

### 回答1: 在UIBot中级中,订单商品导出是指将订单中的商品信息导出成文件的功能。通过使用该功能,可以方便地将订单中的商品信息保存为Excel、CSV或其他格式的文件,以便于后续的分析和处理。 实现订单商品导出的步骤如下: 1. 首先,需要通过UIBot的图形界面或脚本代码来打开订单管理系统,进入到需要导出商品的订单页面。 2. 然后,通过UIBot的录制功能或手动操作的方式,定位到最开始的订单商品信息所在的位置。这可以通过鼠标点击、键盘输入等操作来实现。 3. 接下来,通过UIBot的截屏或OCR(光学字符识别)功能,识别并截取出订单商品信息的区域。如果有多页的商品信息,需要在所有页面上依次进行相同的操作。 4. 在获取到商品信息后,可以使用UIBot的数据处理功能,对商品信息进行整理和处理。例如,可以提取出商品的名称、价格、数量等信息,以便于后续的分析和处理。 5. 最后,通过UIBot的文件处理功能,将整理好的商品信息保存成所需的文件格式。可以选择将商品信息保存为Excel表格、CSV文件或其他文件格式,以便于后续的导入、分析或共享。 通过上述步骤,可以实现在UIBot中级中进行订单商品导出的功能。这个功能可以在各种需要对订单商品进行分析和处理的场景中使用,方便用户快速获取和保存订单中的商品信息。 ### 回答2: uibot中级的订单商品导出功能可以帮助用户快速、方便地将订单中的商品信息导出到指定的文件中。 首先,用户可以通过在uibot的界面中选择订单导出功能来启动该功能。接着,用户需要输入相关参数,如订单号、时间范围等,以便uibot找到要导出的订单。 一旦找到符合条件的订单,uibot将会自动提取订单中的商品信息,并按照用户的要求生成一个可导出的文件,如Excel或CSV文件。用户可以选择导出的文件类型和保存的路径。 导出的文件中,每一行代表一个订单商品,包含了该商品的相关信息,比如商品名称、价格、数量等等。用户可以根据自己的需要选择导出的字段。 通过uibot中级的订单商品导出功能,用户可以很方便地将订单中的商品信息导出到外部文件中,这样就可以更加灵活地进行数据分析、统计以及其他相关操作。同时,使用uibot的自动化功能,还可以大大提高工作效率,节省时间和人力成本。 总之,uibot中级的订单商品导出功能提供了一个快捷、高效的方式,使用户能够轻松地导出订单中的商品信息,并方便地进行后续处理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值