第五天项目
苍穹外卖项目 - 第5天冲刺日志
日期:2025-11-30
冲刺周期:第5天/共7天
会议时间:09:00 - 09:15
会议地点:开发室
参会人员:李靖华 温尚熙 谢斯越 郑哲磊
一、站立会议照片

团队成员正在讨论数据统计功能的实现细节
二、会议内容记录
郑哲磊(后端负责人)
昨天已完成的工作:
- ✅ [WI-049] 完成订单提交接口
- ✅ [WI-050] 实现订单状态管理
- ✅ [WI-051] 完成订单查询和详情接口
- ✅ [WI-052] 实现库存扣减和并发控制
今天计划完成的工作:
- [WI-065] 完成数据统计接口
- [WI-066] 实现WebSocket实时通知
- [WI-067] 完成报表导出功能
- [WI-068] 优化数据库查询性能
工作中遇到的困难:
- 数据统计涉及大量聚合查询,需要优化SQL
- WebSocket连接管理需要考虑断线重连
谢斯越(前端负责人)
昨天已完成的工作:
- ✅ [WI-053] 完成订单管理页面
- ✅ [WI-054] 实现订单状态流转操作
- ✅ [WI-055] 完成订单详情页面
- ✅ [WI-056] 实现数据统计图表
今天计划完成的工作:
- [WI-069] 完成数据统计页面
- [WI-070] 实现报表导出功能
- [WI-071] 优化页面响应速度
- [WI-072] 完善错误提示和用户体验
工作中遇到的困难:
- 大数据量图表渲染性能需要优化
- 报表导出格式需要与后端协商
温尚熙(小程序开发)
昨天已完成的工作:
- ✅ [WI-057] 完成订单详情页面
- ✅ [WI-058] 实现订单支付功能(模拟)
- ✅ [WI-059] 完成订单评价功能
- ✅ [WI-060] 实现订单催单功能
今天计划完成的工作:
- [WI-073] 实现消息通知功能
- [WI-074] 完成用户个人中心
- [WI-075] 优化小程序性能
- [WI-076] 完善异常处理
工作中遇到的困难:
- 小程序消息推送需要申请模板消息权限
- 部分页面在低端设备上加载较慢
李靖华(测试与文档)
昨天已完成的工作:
- ✅ [WI-061] 执行订单模块接口测试
- ✅ [WI-062] 进行并发测试
- ✅ [WI-063] 编写部署文档
- ✅ [WI-064] 进行安全测试
今天计划完成的工作:
- [WI-077] 进行全面集成测试
- [WI-078] 编写用户操作手册
- [WI-079] 进行兼容性测试
- [WI-080] 整理项目文档
工作中遇到的困难:
- 集成测试用例较多,执行时间较长
- 需要在多种设备和浏览器上进行兼容性测试
三、燃尽图
剩余工作量(小时)
120 |●
| \
100 | ●
| \\
80 | \
| ●
60 | \\
| \
40 | ●
| \\
20 | ●
| \
0 |________________________________●
1 2 3 4 5 6 7 (天数)
图例:
● —— 实际进度(实线)
- - - 理想进度(虚线)
燃尽图说明:
- 当前剩余工作量:18小时(完成102小时)
- 理想剩余工作量:26小时
- 进度状态:✅ 显著快于预期进度
- 燃尽速度:24小时/天
项目收敛分析:
- 第5天项目进入收尾阶段,主要功能已全部完成
- 实际进度大幅领先理想进度,项目提前进入测试和优化阶段
- 剩余工作主要是测试、优化和文档整理
- 预计可以提前1天完成所有任务
四、代码/文档签入记录
温尚熙 - 数据统计与WebSocket实时通知模块
- 模块名称:sky-server 数据统计与WebSocket模块
- 提交内容:
- 完成营业额统计接口
- 实现订单统计和用户统计
- 添加WebSocket服务端
- 优化统计查询SQL
代码示例:
// ReportController.java - 数据统计控制器
@RestController
@RequestMapping("/admin/report")
@Slf4j
@Api(tags = "数据统计相关接口")
public class ReportController {
@Autowired
private 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);
}
@GetMapping("/ordersStatistics")
@ApiOperation("订单统计")
public Result<OrderReportVO> ordersStatistics(
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {
OrderReportVO orderReportVO = reportService.getOrderStatistics(begin, end);
return Result.success(orderReportVO);
}
@GetMapping("/userStatistics")
@ApiOperation("用户统计")
public Result<UserReportVO> userStatistics(
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {
UserReportVO userReportVO = reportService.getUserStatistics(begin, end);
return Result.success(userReportVO);
}
@GetMapping("/export")
@ApiOperation("导出运营数据报表")
public void export(HttpServletResponse response) {
reportService.exportBusinessData(response);
}
}
// ReportServiceImpl.java - 数据统计业务逻辑
@Service
@Slf4j
public class ReportServiceImpl implements ReportService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate redisTemplate;
public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {
// 先从Redis缓存获取
String cacheKey = "report:turnover:" + begin + ":" + end;
TurnoverReportVO cached = (TurnoverReportVO) redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached;
}
// 构建日期列表
List<LocalDate> dateList = new ArrayList<>();
dateList.add(begin);
while (!begin.equals(end)) {
begin = begin.plusDays(1);
dateList.add(begin);
}
// 查询每天的营业额
List<Double> turnoverList = new ArrayList<>();
for (LocalDate date : dateList) {
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
// 查询当天已完成订单的总金额
Map map = new HashMap();
map.put("begin", beginTime);
map.put("end", endTime);
map.put("status", Orders.COMPLETED);
Double turnover = orderMapper.sumByMap(map);
turnover = turnover == null ? 0.0 : turnover;
turnoverList.add(turnover);
}
// 封装返回结果
TurnoverReportVO turnoverReportVO = TurnoverReportVO.builder()
.dateList(StringUtils.join(dateList, ","))
.turnoverList(StringUtils.join(turnoverList, ","))
.build();
// 缓存结果(1小时)
redisTemplate.opsForValue().set(cacheKey, turnoverReportVO, 1, TimeUnit.HOURS);
return turnoverReportVO;
}
/**
* 导出运营数据报表
*/
public void exportBusinessData(HttpServletResponse response) {
// 查询最近30天的运营数据
LocalDate dateBegin = LocalDate.now().minusDays(30);
LocalDate dateEnd = LocalDate.now().minusDays(1);
// 查询概览数据
BusinessDataVO businessDataVO = getBusinessData(dateBegin, dateEnd);
// 通过POI将数据写入Excel文件
InputStream in = this.getClass().getClassLoader()
.getResourceAsStream("template/运营数据报表模板.xlsx");
try {
XSSFWorkbook excel = new XSSFWorkbook(in);
XSSFSheet sheet = excel.getSheet("Sheet1");
// 填充数据
sheet.getRow(1).getCell(1).setCellValue("时间:" + dateBegin + "至" + dateEnd);
sheet.getRow(3).getCell(2).setCellValue(businessDataVO.getTurnover());
sheet.getRow(3).getCell(4).setCellValue(businessDataVO.getOrderCompletionRate());
sheet.getRow(3).getCell(6).setCellValue(businessDataVO.getNewUsers());
// 通过输出流将Excel文件下载到客户端浏览器
ServletOutputStream out = response.getOutputStream();
excel.write(out);
out.close();
excel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// WebSocketServer.java - WebSocket服务端
@Component
@ServerEndpoint("/ws/{userId}")
@Slf4j
public class WebSocketServer {
// 存放会话对象
private static Map<String, Session> sessionMap = new HashMap();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
log.info("客户端:{} 建立连接", userId);
sessionMap.put(userId, session);
}
/**
* 收到客户端消息后调用的方法
*/
@OnMessage
public void onMessage(String message, @PathParam("userId") String userId) {
log.info("收到来自客户端:{} 的信息:{}", userId, message);
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(@PathParam("userId") String userId) {
log.info("连接断开:{}", userId);
sessionMap.remove(userId);
}
/**
* 群发消息
*/
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();
for (Session session : sessions) {
try {
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 向指定客户端发送消息
*/
public void sendToClient(String userId, String message) {
Session session = sessionMap.get(userId);
if (session != null) {
try {
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
<!-- OrderMapper.xml - 统计查询SQL优化 -->
<mapper namespace="com.sky.mapper.OrderMapper">
<!-- 根据条件统计营业额 -->
<select id="sumByMap" resultType="java.lang.Double">
SELECT SUM(amount) FROM orders
<where>
<if test="begin != null">
AND order_time >= #{begin}
</if>
<if test="end != null">
AND order_time <= #{end}
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
</select>
<!-- 添加索引优化查询性能 -->
<!-- CREATE INDEX idx_order_time_status ON orders(order_time, status); -->
</mapper>
李靖华 - 管理端数据统计页面模块
- 模块名称:sky-admin 数据统计页面
- 提交内容:
- 完成数据统计页面
- 实现报表导出功能
- 优化图表渲染性能
- 完善错误提示
代码示例:
<!-- Statistics.vue - 数据统计页面 -->
<template>
<div class="statistics-container">
<!-- 日期选择 -->
<el-card class="filter-card">
<el-form :inline="true">
<el-form-item label="统计日期">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="handleDateChange"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button @click="handleExport">导出报表</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 数据概览 -->
<el-row :gutter="20" class="overview-row">
<el-col :span="6">
<el-card>
<div class="stat-item">
<div class="stat-label">营业额</div>
<div class="stat-value">¥{{ overview.turnover?.toFixed(2) }}</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card>
<div class="stat-item">
<div class="stat-label">订单数</div>
<div class="stat-value">{{ overview.orderCount }}</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card>
<div class="stat-item">
<div class="stat-label">新增用户</div>
<div class="stat-value">{{ overview.newUsers }}</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card>
<div class="stat-item">
<div class="stat-label">订单完成率</div>
<div class="stat-value">{{ overview.orderCompletionRate }}%</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 营业额趋势图 -->
<el-card class="chart-card">
<template #header>
<span>营业额趋势</span>
</template>
<div ref="turnoverChartRef" style="height: 400px"></div>
</el-card>
<!-- 订单统计图 -->
<el-card class="chart-card">
<template #header>
<span>订单统计</span>
</template>
<div ref="orderChartRef" style="height: 400px"></div>
</el-card>
<!-- 用户增长图 -->
<el-card class="chart-card">
<template #header>
<span>用户增长</span>
</template>
<div ref="userChartRef" style="height: 400px"></div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import { getTurnoverStatisticsApi, getOrderStatisticsApi,
getUserStatisticsApi, exportReportApi } from '@/api/report'
import { ElMessage } from 'element-plus'
const dateRange = ref([])
const overview = reactive({
turnover: 0,
orderCount: 0,
newUsers: 0,
orderCompletionRate: 0
})
const turnoverChartRef = ref(null)
const orderChartRef = ref(null)
const userChartRef = ref(null)
let turnoverChart = null
let orderChart = null
let userChart = null
// 初始化图表
const initCharts = () => {
turnoverChart = echarts.init(turnoverChartRef.value)
orderChart = echarts.init(orderChartRef.value)
userChart = echarts.init(userChartRef.value)
// 响应式调整
window.addEventListener('resize', () => {
turnoverChart?.resize()
orderChart?.resize()
userChart?.resize()
})
}
// 加载营业额数据
const loadTurnoverData = async () => {
const [begin, end] = dateRange.value
const { data } = await getTurnoverStatisticsApi({
begin: begin.toISOString().split('T')[0],
end: end.toISOString().split('T')[0]
})
const dateList = data.dateList.split(',')
const turnoverList = data.turnoverList.split(',').map(Number)
// 更新概览数据
overview.turnover = turnoverList.reduce((a, b) => a + b, 0)
// 渲染图表
turnoverChart.setOption({
title: { text: '营业额趋势' },
tooltip: {
trigger: 'axis',
formatter: (params) => {
return `${params[0].name}<br/>营业额: ¥${params[0].value.toFixed(2)}`
}
},
xAxis: {
type: 'category',
data: dateList
},
yAxis: {
type: 'value',
axisLabel: {
formatter: '¥{value}'
}
},
series: [{
name: '营业额',
type: 'line',
data: turnoverList,
smooth: true,
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(24, 144, 255, 0.3)' },
{ offset: 1, color: 'rgba(24, 144, 255, 0.1)' }
])
}
}]
})
}
// 加载订单数据
const loadOrderData = async () => {
const [begin, end] = dateRange.value
const { data } = await getOrderStatisticsApi({
begin: begin.toISOString().split('T')[0],
end: end.toISOString().split('T')[0]
})
const dateList = data.dateList.split(',')
const orderCountList = data.orderCountList.split(',').map(Number)
const validOrderCountList = data.validOrderCountList.split(',').map(Number)
// 更新概览数据
overview.orderCount = orderCountList.reduce((a, b) => a + b, 0)
const validOrderCount = validOrderCountList.reduce((a, b) => a + b, 0)
overview.orderCompletionRate = ((validOrderCount / overview.orderCount) * 100).toFixed(2)
// 渲染图表
orderChart.setOption({
title: { text: '订单统计' },
tooltip: { trigger: 'axis' },
legend: { data: ['订单总数', '有效订单'] },
xAxis: {
type: 'category',
data: dateList
},
yAxis: { type: 'value' },
series: [
{
name: '订单总数',
type: 'bar',
data: orderCountList
},
{
name: '有效订单',
type: 'bar',
data: validOrderCountList
}
]
})
}
// 加载用户数据
const loadUserData = async () => {
const [begin, end] = dateRange.value
const { data } = await getUserStatisticsApi({
begin: begin.toISOString().split('T')[0],
end: end.toISOString().split('T')[0]
})
const dateList = data.dateList.split(',')
const newUserList = data.newUserList.split(',').map(Number)
const totalUserList = data.totalUserList.split(',').map(Number)
// 更新概览数据
overview.newUsers = newUserList.reduce((a, b) => a + b, 0)
// 渲染图表
userChart.setOption({
title: { text: '用户增长' },
tooltip: { trigger: 'axis' },
legend: { data: ['新增用户', '总用户数'] },
xAxis: {
type: 'category',
data: dateList
},
yAxis: { type: 'value' },
series: [
{
name: '新增用户',
type: 'line',
data: newUserList
},
{
name: '总用户数',
type: 'line',
data: totalUserList
}
]
})
}
// 加载所有数据
const loadData = async () => {
if (!dateRange.value || dateRange.value.length !== 2) {
ElMessage.warning('请选择日期范围')
return
}
try {
await Promise.all([
loadTurnoverData(),
loadOrderData(),
loadUserData()
])
ElMessage.success('数据加载成功')
} catch (error) {
ElMessage.error('数据加载失败')
}
}
// 导出报表
const handleExport = async () => {
try {
const response = await exportReportApi()
const blob = new Blob([response.data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `运营数据报表_${new Date().getTime()}.xlsx`
link.click()
window.URL.revokeObjectURL(url)
ElMessage.success('导出成功')
} catch (error) {
ElMessage.error('导出失败')
}
}
onMounted(() => {
// 默认最近7天
const end = new Date()
const begin = new Date()
begin.setDate(begin.getDate() - 7)
dateRange.value = [begin, end]
nextTick(() => {
initCharts()
loadData()
})
})
</script>
<style scoped>
.statistics-container {
padding: 20px;
}
.filter-card {
margin-bottom: 20px;
}
.overview-row {
margin-bottom: 20px;
}
.stat-item {
text-align: center;
}
.stat-label {
font-size: 14px;
color: #666;
margin-bottom: 10px;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #1890ff;
}
.chart-card {
margin-bottom: 20px;
}
</style>
谢斯越
- 提交内容:
- 完成消息通知功能
- 实现用户个人中心
- 优化小程序性能
- 完善异常处理
郑哲磊
- 提交内容:
- 完成全面集成测试
- 编写用户操作手册
- 进行兼容性测试
- 整理项目文档
浙公网安备 33010602011771号