第五天项目

苍穹外卖项目 - 第5天冲刺日志

日期:2025-11-30
冲刺周期:第5天/共7天
会议时间:09:00 - 09:15
会议地点:开发室
参会人员:李靖华 温尚熙 谢斯越 郑哲磊


一、站立会议照片

440be925a0c6a3541faec4ff732b18f5

团队成员正在讨论数据统计功能的实现细节


二、会议内容记录

郑哲磊(后端负责人)

昨天已完成的工作

  • ✅ [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 &gt;= #{begin}
            </if>
            <if test="end != null">
                AND order_time &lt;= #{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>

谢斯越

  • 提交内容
    • 完成消息通知功能
    • 实现用户个人中心
    • 优化小程序性能
    • 完善异常处理

郑哲磊

  • 提交内容
    • 完成全面集成测试
    • 编写用户操作手册
    • 进行兼容性测试
    • 整理项目文档
posted @ 2025-12-03 19:59  清月明风  阅读(5)  评论(0)    收藏  举报