阶段:苍穹外卖 · 管理端功能完善 & 项目完结
今日完成
一、数据统计接口(ReportController)
在 Day9 完成了营业额统计的基础上,继续补齐剩余的统计接口。
1. 用户统计 /admin/report/userStatistics
根据日期范围统计每日新增用户数和累计用户总数:
// 核心逻辑:遍历日期列表,每天查两次
for (LocalDate date : dateList) {
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
Map map = new HashedMap();
map.put("end", endTime);
Integer totalUser = userMapper.countByMap(map); // 累计用户(只传end)
map.put("begin", beginTime);
Integer newUser = userMapper.countByMap(map); // 新增用户(传begin+end)
totalUserList.add(totalUser);
newUserList.add(newUser);
}
UserMapper.xml 中的 countByMap 是一个通用计数方法,通过动态 <where> 拼接条件:
<select id="countByMap" resultType="java.lang.Integer">
select count(id) from user
<where>
<if test="begin != null">
and create_time > #{begin}
</if>
<if test="end != null">
and create_time < #{end}
</if>
</where>
</select>
小技巧:累计用户只传 end 参数,不传 begin,这样 <where> 只拼接 create_time < end,查出的是截止到当天的所有用户。
2. 订单统计 /admin/report/ordersStatistics
统计每日订单数、有效订单数,并计算订单完成率:
for (LocalDate date : dateList) {
Integer orderCount = getOrderCount(beginTime, endTime, null); // 全部订单
Integer validOrderCount = getOrderCount(beginTime, endTime, Orders.COMPLETED); // 有效订单
}
// 订单完成率 = 有效订单数 / 订单总数
Double orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;
复用了 getOrderCount 私有方法,通过传入不同的 status 参数来区分查询条件。
3. 销量排行榜 Top10 /admin/report/top10
查询指定时间范围内销量最高的10个菜品/套餐:
<select id="getSalesTop10" resultType="com.sky.dto.GoodsSalesDTO">
select od.name, sum(od.number) number
from order_detail od, orders o
where od.order_id = o.id and o.status = 5
<if test="begin != null">
and o.order_time > #{begin}
</if>
<if test="end != null">
and o.order_time < #{end}
</if>
group by od.name
order by number desc
limit 0,10
</select>
SQL 要点:
- order_detail 和 orders 表联查,只统计已完成订单(status=5)
- GROUP BY od.name 按菜品名称分组
- SUM(od.number) 累计销量
- ORDER BY number DESC LIMIT 0,10 取前10
Service 层用 Stream 提取名称和数量列表:
List<GoodsSalesDTO> salesTop10 = orderMapper.getSalesTop10(beginTime, endTime);
List<String> names = salesTop10.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());
List<Integer> numbers = salesTop10.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());
二、工作台接口(WorkSpaceController)
工作台是管理端首页的数据概览面板,展示今日经营数据和各模块总览。
1. 今日营业数据 /admin/workspace/businessData
一次性返回5个核心指标:
| 营业额 | turnover | 当日已完成订单的金额总和 |
| 有效订单数 | validOrderCount | 当日已完成订单的数量 |
| 订单完成率 | orderCompletionRate | 有效订单数 / 总订单数 |
| 平均客单价 | unitPrice | 营业额 / 有效订单数 |
| 新增用户数 | newUsers | 当日新注册用户数 |
public BusinessDataVO getBusinessData(LocalDateTime begin, LocalDateTime end) {
Map map = new HashMap();
map.put("begin", begin);
map.put("end", end);
Integer totalOrderCount = orderMapper.countByMap(map); // 总订单数
map.put("status", Orders.COMPLETED);
Double turnover = orderMapper.sumByMap(map); // 营业额
Integer validOrderCount = orderMapper.countByMap(map); // 有效订单数
// 避免除以0
if (totalOrderCount != 0 && validOrderCount != 0) {
orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;
unitPrice = turnover / validOrderCount;
}
Integer newUsers = userMapper.countByMap(map); // 新增用户
return BusinessDataVO.builder()...build();
}
2. 订单总览 /admin/workspace/overviewOrders
查询当日各状态的订单数量:待接单、待派送、已完成、已取消、全部。
public OrderOverViewVO getOrderOverView() {
map.put("begin", LocalDateTime.now().with(LocalTime.MIN));
map.put("status", Orders.TO_BE_CONFIRMED);
Integer waitingOrders = orderMapper.countByMap(map); // 待接单
map.put("status", Orders.CONFIRMED);
Integer deliveredOrders = orderMapper.countByMap(map); // 待派送
map.put("status", Orders.COMPLETED);
Integer completedOrders = orderMapper.countByMap(map); // 已完成
map.put("status", Orders.CANCELLED);
Integer cancelledOrders = orderMapper.countByMap(map); // 已取消
map.put("status", null);
Integer allOrders = orderMapper.countByMap(map); // 全部
}
3. 菜品/套餐总览
查询已起售和已停售的数量,逻辑很简单:
public DishOverViewVO getDishOverView() {
map.put("status", StatusConstant.ENABLE);
Integer sold = dishMapper.countByMap(map); // 已起售
map.put("status", StatusConstant.DISABLE);
Integer discontinued = dishMapper.countByMap(map); // 已停售
}
三、Excel报表导出 /admin/report/export
使用 Apache POI 基于模板文件导出最近30天的运营数据报表。
实现流程
读取Excel模板 → 填充汇总数据 → 填充30天明细 → 输出到浏览器下载
public void exportBusinessData(HttpServletResponse response) {
// 1. 查询最近30天数据
LocalDate dateBegin = LocalDate.now().minusDays(30);
LocalDate dateEnd = LocalDate.now().minusDays(1);
BusinessDataVO businessDatavo = workspaceService.getBusinessData(...);
// 2. 基于模板创建Excel
InputStream in = this.getClass().getClassLoader()
.getResourceAsStream("template/运营数据报表模板.xlsx");
XSSFWorkbook excel = new XSSFWorkbook(in);
XSSFSheet sheet = excel.getSheet("Sheet1");
// 3. 填充汇总行
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());
// 4. 填充30天明细
for (int i = 0; i < 30; i++) {
LocalDate date = dateBegin.plusDays(i);
BusinessDataVO data = workspaceService.getBusinessData(...);
row = sheet.getRow(7 + i);
row.getCell(1).setCellValue(date.toString());
row.getCell(2).setCellValue(data.getTurnover());
// … 填充其他列
}
// 5. 输出流下载
ServletOutputStream out = response.getOutputStream();
excel.write(out);
out.close();
excel.close();
}
POI 要点:
- XSSFWorkbook 操作 .xlsx 格式
- 基于模板文件避免从零创建样式
- response.getOutputStream() 直接写入响应流,浏览器触发下载
四、项目文档更新
最后更新了 README.md 和 README.en.md,完整记录了项目的所有功能:
- 管理端 9 个 Controller:员工、分类、菜品、套餐、订单、文件上传、店铺、数据统计、工作台
- 用户端 8 个 Controller:微信登录、分类、菜品、套餐、购物车、地址簿、订单、店铺
- 支付回调 1 个 Controller
- 11 张数据库表及表关系
- 核心技术实现:JWT双端鉴权、AOP自动填充、Redis缓存、微信支付、WebSocket推送、定时任务
- 18 个功能模块全部标记为已完成
技术总结
统计接口的设计思路
所有统计接口都遵循同一个模式:
1. 生成日期列表(begin → end)
2. 遍历日期,每天调用 Mapper 查询
3. 结果用逗号拼接成字符串返回
为什么用逗号拼接字符串而不是返回 List?因为前端 ECharts 图表直接接收逗号分隔的字符串作为数据源,这样前端拿到数据就能直接用,不需要再处理。
通用查询方法的复用
OrderMapper.countByMap 和 OrderMapper.sumByMap 是两个万能查询方法,通过传入不同的 Map 参数组合,覆盖了几乎所有统计场景:
| 当日总订单数 | ✓ | ✓ | ✗ |
| 当日有效订单数 | ✓ | ✓ | ✓(COMPLETED) |
| 当日营业额 | ✓ | ✓ | ✓(COMPLETED) |
| 截止累计用户 | ✗ | ✓ | – |
| 当日新增用户 | ✓ | ✓ | – |
动态 SQL 的 <if> 标签让一个方法顶五个用,代码复用率很高。
项目完结感想
从 Day1 到 Day10,苍穹外卖项目终于画上了句号。
完成的功能模块:
| 1 | 员工管理 | JWT登录、MD5加密、分页查询 |
| 2 | 分类管理 | CRUD、启停用 |
| 3 | 菜品管理 | 事务、口味关联、批量删除、Redis缓存 |
| 4 | 套餐管理 | 事务、菜品关联、级联停售 |
| 5 | 文件上传 | 阿里云OSS |
| 6 | 店铺管理 | Redis存储营业状态 |
| 7 | 微信登录 | code2session、自动注册 |
| 8 | 购物车 | 增删改查、数量管理 |
| 9 | 地址簿 | CRUD、默认地址唯一性 |
| 10 | 订单管理 | 提交、支付、退款、取消、再来一单 |
| 11 | 微信支付 | 预支付、AES解密回调、退款 |
| 12 | 订单管理(管理端) | 搜索、接单、拒单、派送、完成 |
| 13 | 定时任务 | 超时取消、自动完成 |
| 14 | WebSocket | 来单提醒、催单推送 |
| 15 | 数据统计 | 营业额、用户、订单、销量Top10 |
| 16 | Excel导出 | Apache POI模板导出 |
| 17 | 工作台 | 今日数据、菜品/套餐/订单总览 |
| 18 | AOP自动填充 | @AutoFill注解、反射调用setter |
技术栈全覆盖:Spring Boot、MyBatis、MySQL、Redis、JWT、AOP、WebSocket、定时任务、Apache POI、阿里云OSS、微信支付。
这个项目让我从"会写CRUD"进化到了"能独立搭建一个完整的后端系统"。接下来要开始学习 Redis + 黑马点评,继续深入!
Day10 完成,苍穹外卖项目完结。




