第六天项目

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

日期:2025-12- 01
冲刺周期:第6天/共7天
参会人员:李靖华 温尚熙 谢斯越 郑哲磊

二、会议内容记录

郑哲磊(后端负责人)

昨天已完成的工作

  • ✅ [WI-065] 完成数据统计接口
  • ✅ [WI-066] 实现WebSocket实时通知
  • ✅ [WI-067] 完成报表导出功能
  • ✅ [WI-068] 优化数据库查询性能

今天计划完成的工作

  • [WI-081] 修复测试中发现的Bug
  • [WI-082] 进行代码重构和优化
  • [WI-083] 完善API文档
  • [WI-084] 进行最终代码审查

工作中遇到的困难

  • 暂无,主要是优化和完善工作

谢斯越(前端负责人)

昨天已完成的工作

  • ✅ [WI-069] 完成数据统计页面
  • ✅ [WI-070] 实现报表导出功能
  • ✅ [WI-071] 优化页面响应速度
  • ✅ [WI-072] 完善错误提示和用户体验

今天计划完成的工作

  • [WI-085] 优化前端性能
  • [WI-086] 完善用户交互体验
  • [WI-087] 修复UI细节问题
  • [WI-088] 进行最终测试

工作中遇到的困难

  • 暂无,主要是细节优化工作

温尚熙(小程序开发)

昨天已完成的工作

  • ✅ [WI-073] 实现消息通知功能
  • ✅ [WI-074] 完成用户个人中心
  • ✅ [WI-075] 优化小程序性能
  • ✅ [WI-076] 完善异常处理

今天计划完成的工作

  • [WI-089] 优化小程序用户体验
  • [WI-090] 完善小程序文档
  • [WI-091] 进行真机测试
  • [WI-092] 准备小程序发布

工作中遇到的困难

  • 暂无,主要是测试和准备发布工作

李靖华(测试与文档)

昨天已完成的工作

  • ✅ [WI-077] 进行全面集成测试
  • ✅ [WI-078] 编写用户操作手册
  • ✅ [WI-079] 进行兼容性测试
  • ✅ [WI-080] 整理项目文档

今天计划完成的工作

  • [WI-093] 进行回归测试
  • [WI-094] 完善项目文档
  • [WI-095] 编写项目总结报告
  • [WI-096] 准备项目演示材料

工作中遇到的困难

  • 暂无,主要是文档整理和总结工作

三、燃尽图

剩余工作量(小时)
120 |●
    |  \
100 |    ●
    |      \\
 80 |          \
    |            ●
 60 |              \\
    |                  \
 40 |                    ●
    |                      \\
 20 |                          ●
    |                            \
  0 |________________________________●●
    1   2   3   4   5   6   7  (天数)

图例:
● —— 实际进度(实线)
- - - 理想进度(虚线)

燃尽图说明

  • 当前剩余工作量:6小时(完成114小时)
  • 理想剩余工作量:17小时
  • 进度状态:✅ 大幅领先预期进度
  • 燃尽速度:12小时/天(收尾阶段)

四、代码/文档签入记录

李靖华 温尚熙 - Bug修复与代码优化模块

  • 模块名称:sky-server Bug修复与代码重构
  • 提交内容
    • 修复测试中发现的3个Bug
    • 重构部分冗余代码
    • 优化异常处理逻辑
    • 完善API文档注释

代码示例

// Bug修复1: 订单状态更新并发问题
// OrderServiceImpl.java - 修复前
@Transactional
public void updateStatus(Long orderId, Integer status) {
    Orders orders = orderMapper.getById(orderId);
    orders.setStatus(status);
    orderMapper.update(orders);  // 可能出现并发问题
}

// OrderServiceImpl.java - 修复后(使用乐观锁)
@Transactional
public void updateStatus(Long orderId, Integer status) {
    Orders orders = orderMapper.getById(orderId);
    orders.setStatus(status);
    orders.setUpdateTime(LocalDateTime.now());
    
    // 使用乐观锁更新,防止并发问题
    int rows = orderMapper.updateWithVersion(orders);
    if (rows == 0) {
        throw new OrderBusinessException("订单状态已被修改,请刷新后重试");
    }
}
<!-- OrderMapper.xml - 添加乐观锁 -->
<update id="updateWithVersion">
    UPDATE orders
    SET status = #{status},
        update_time = #{updateTime},
        version = version + 1
    WHERE id = #{id} AND version = #{version}
</update>
// Bug修复2: 统计数据精度问题
// ReportServiceImpl.java - 修复前
public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {
    Double turnover = orderMapper.sumByMap(map);
    turnover = turnover == null ? 0.0 : turnover;  // 精度丢失
    turnoverList.add(turnover);
}

// ReportServiceImpl.java - 修复后(使用BigDecimal)
public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {
    BigDecimal turnover = orderMapper.sumByMapWithDecimal(map);
    turnover = turnover == null ? BigDecimal.ZERO : turnover;
    // 保留2位小数
    turnoverList.add(turnover.setScale(2, RoundingMode.HALF_UP));
}
// Bug修复3: WebSocket断线重连逻辑
// WebSocketServer.java - 修复前
@OnClose
public void onClose(@PathParam("userId") String userId) {
    sessionMap.remove(userId);  // 直接移除,没有重连机制
}

// WebSocketServer.java - 修复后(添加心跳检测)
@Component
@ServerEndpoint("/ws/{userId}")
@Slf4j
public class WebSocketServer {
    private static Map<String, Session> sessionMap = new ConcurrentHashMap<>();
    private static Map<String, Long> heartbeatMap = new ConcurrentHashMap<>();
    
    // 心跳检测定时任务
    @Scheduled(fixedRate = 30000)  // 每30秒检测一次
    public void heartbeatCheck() {
        long currentTime = System.currentTimeMillis();
        Iterator<Map.Entry<String, Long>> iterator = heartbeatMap.entrySet().iterator();
        
        while (iterator.hasNext()) {
            Map.Entry<String, Long> entry = iterator.next();
            String userId = entry.getKey();
            Long lastHeartbeat = entry.getValue();
            
            // 超过60秒没有心跳,认为连接断开
            if (currentTime - lastHeartbeat > 60000) {
                log.warn("用户 {} 心跳超时,移除连接", userId);
                sessionMap.remove(userId);
                iterator.remove();
            }
        }
    }
    
    @OnMessage
    public void onMessage(String message, @PathParam("userId") String userId) {
        if ("ping".equals(message)) {
            // 更新心跳时间
            heartbeatMap.put(userId, System.currentTimeMillis());
            try {
                Session session = sessionMap.get(userId);
                if (session != null) {
                    session.getBasicRemote().sendText("pong");
                }
            } catch (IOException e) {
                log.error("发送心跳响应失败", e);
            }
        }
    }
}
// 代码重构: 提取公共方法,减少重复代码
// BaseService.java - 新增基础服务类
@Service
public abstract class BaseService<T> {
    
    /**
     * 分页查询通用方法
     */
    protected PageResult pageQuery(PageQueryDTO pageQueryDTO, 
                                   Function<PageQueryDTO, List<T>> queryFunction,
                                   Function<PageQueryDTO, Long> countFunction) {
        // 设置分页参数
        PageHelper.startPage(pageQueryDTO.getPage(), pageQueryDTO.getPageSize());
        
        // 执行查询
        List<T> records = queryFunction.apply(pageQueryDTO);
        Page<T> page = (Page<T>) records;
        
        // 封装结果
        return new PageResult(page.getTotal(), page.getResult());
    }
    
    /**
     * 批量操作通用方法
     */
    @Transactional
    protected void batchOperation(List<Long> ids, Consumer<Long> operation) {
        if (ids == null || ids.isEmpty()) {
            throw new BaseException("操作对象不能为空");
        }
        
        for (Long id : ids) {
            try {
                operation.accept(id);
            } catch (Exception e) {
                log.error("批量操作失败,id: {}", id, e);
                throw new BaseException("批量操作失败");
            }
        }
    }
}
// 优化异常处理: 全局异常处理器增强
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    /**
     * 处理业务异常
     */
    @ExceptionHandler(BaseException.class)
    public Result handleBusinessException(BaseException ex) {
        log.error("业务异常:{}", ex.getMessage());
        return Result.error(ex.getMessage());
    }
    
    /**
     * 处理参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handleValidationException(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder sb = new StringBuilder("参数校验失败:");
        
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append(fieldError.getField())
              .append(":")
              .append(fieldError.getDefaultMessage())
              .append(";");
        }
        
        log.error(sb.toString());
        return Result.error(sb.toString());
    }
    
    /**
     * 处理SQL异常
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public Result handleSQLException(SQLIntegrityConstraintViolationException ex) {
        String message = ex.getMessage();
        if (message.contains("Duplicate entry")) {
            String[] split = message.split(" ");
            String duplicateValue = split[2];
            return Result.error(duplicateValue + " 已存在");
        }
        return Result.error("数据库操作失败");
    }
    
    /**
     * 处理未知异常
     */
    @ExceptionHandler(Exception.class)
    public Result handleException(Exception ex) {
        log.error("系统异常:", ex);
        return Result.error("系统繁忙,请稍后再试");
    }
}
// 完善API文档注释
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品管理接口")
@Slf4j
public class DishController {
    
    @Autowired
    private DishService dishService;
    
    @PostMapping
    @ApiOperation(value = "新增菜品", notes = "新增菜品及其口味信息")
    @ApiImplicitParams({
        @ApiImplicitParam(name = "dishDTO", value = "菜品信息", required = true, 
                         dataType = "DishDTO", paramType = "body")
    })
    @ApiResponses({
        @ApiResponse(code = 200, message = "操作成功"),
        @ApiResponse(code = 400, message = "参数错误"),
        @ApiResponse(code = 500, message = "系统异常")
    })
    public Result save(@RequestBody @Validated DishDTO dishDTO) {
        log.info("新增菜品:{}", dishDTO);
        dishService.saveWithFlavor(dishDTO);
        return Result.success();
    }
}

谢斯越 郑哲磊 - 管理端性能优化模块

  • 模块名称:sky-admin 性能优化与用户体验
  • 提交内容
    • 优化组件渲染性能
    • 完善用户交互反馈
    • 修复UI细节问题
    • 添加骨架屏加载效果

代码示例

<!-- 性能优化1: 虚拟滚动优化长列表 -->
<!-- OrderList.vue - 优化前 -->
<template>
  <el-table :data="orderList">
    <!-- 数据量大时渲染慢 -->
    <el-table-column v-for="column in columns" :key="column.prop" />
  </el-table>
</template>

<!-- OrderList.vue - 优化后(使用虚拟滚动) -->
<template>
  <div class="virtual-list-container">
    <el-table-v2
      :columns="columns"
      :data="orderList"
      :width="1200"
      :height="600"
      :row-height="60"
      fixed
    >
      <template #cell="{ column, rowData }">
        <div v-if="column.key === 'status'">
          <el-tag :type="getStatusType(rowData.status)">
            {{ getStatusText(rowData.status) }}
          </el-tag>
        </div>
        <div v-else>{{ rowData[column.key] }}</div>
      </template>
    </el-table-v2>
  </div>
</template>

<script setup>
import { ElTableV2 } from 'element-plus'

const columns = [
  { key: 'number', title: '订单号', width: 180 },
  { key: 'consignee', title: '收货人', width: 120 },
  { key: 'phone', title: '手机号', width: 120 },
  { key: 'amount', title: '金额', width: 100 },
  { key: 'status', title: '状态', width: 100 },
  { key: 'orderTime', title: '下单时间', width: 180 }
]
</script>
<!-- 性能优化2: 骨架屏加载效果 -->
<!-- components/Skeleton.vue - 骨架屏组件 -->
<template>
  <div class="skeleton-container">
    <div v-if="loading" class="skeleton">
      <div class="skeleton-header">
        <div class="skeleton-avatar"></div>
        <div class="skeleton-content">
          <div class="skeleton-title"></div>
          <div class="skeleton-subtitle"></div>
        </div>
      </div>
      <div class="skeleton-body">
        <div v-for="i in rows" :key="i" class="skeleton-row"></div>
      </div>
    </div>
    <slot v-else></slot>
  </div>
</template>

<script setup>
defineProps({
  loading: {
    type: Boolean,
    default: true
  },
  rows: {
    type: Number,
    default: 5
  }
})
</script>

<style scoped>
.skeleton {
  padding: 20px;
  background: #fff;
  border-radius: 4px;
}

.skeleton-avatar,
.skeleton-title,
.skeleton-subtitle,
.skeleton-row {
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: loading 1.5s ease-in-out infinite;
}

@keyframes loading {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

.skeleton-avatar {
  width: 60px;
  height: 60px;
  border-radius: 50%;
}

.skeleton-title {
  height: 20px;
  width: 60%;
  margin-bottom: 10px;
  border-radius: 4px;
}

.skeleton-row {
  height: 16px;
  margin-bottom: 12px;
  border-radius: 4px;
}
</style>
<!-- 使用骨架屏 -->
<template>
  <Skeleton :loading="loading" :rows="10">
    <el-table :data="orderList">
      <!-- 实际内容 -->
    </el-table>
  </Skeleton>
</template>
<!-- 性能优化3: 图片懒加载优化 -->
<!-- components/LazyImage.vue - 图片懒加载组件 -->
<template>
  <div class="lazy-image-wrapper" ref="wrapperRef">
    <img
      v-if="isLoaded"
      :src="src"
      :alt="alt"
      class="lazy-image"
      @load="handleLoad"
      @error="handleError"
    />
    <div v-else class="lazy-image-placeholder">
      <el-icon class="loading-icon"><Loading /></el-icon>
    </div>
    <img v-if="error" :src="errorImage" class="lazy-image-error" />
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { Loading } from '@element-plus/icons-vue'

const props = defineProps({
  src: String,
  alt: String,
  errorImage: {
    type: String,
    default: '/images/error.png'
  }
})

const wrapperRef = ref(null)
const isLoaded = ref(false)
const error = ref(false)

let observer = null

const handleLoad = () => {
  isLoaded.value = true
}

const handleError = () => {
  error.value = true
}

onMounted(() => {
  // 使用 IntersectionObserver 实现懒加载
  observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting && !isLoaded.value) {
        isLoaded.value = true
        observer.unobserve(entry.target)
      }
    })
  }, {
    rootMargin: '50px'  // 提前50px开始加载
  })
  
  if (wrapperRef.value) {
    observer.observe(wrapperRef.value)
  }
})

onUnmounted(() => {
  if (observer && wrapperRef.value) {
    observer.unobserve(wrapperRef.value)
  }
})
</script>
<!-- 性能优化4: 防抖和节流 -->
<!-- composables/useDebounce.js - 防抖Hook -->
<script>
import { ref } from 'vue'

export function useDebounce(fn, delay = 300) {
  let timer = null
  
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

export function useThrottle(fn, delay = 300) {
  let lastTime = 0
  
  return function(...args) {
    const now = Date.now()
    if (now - lastTime >= delay) {
      fn.apply(this, args)
      lastTime = now
    }
  }
}
</script>

<!-- 使用示例 -->
<template>
  <el-input
    v-model="searchKey"
    placeholder="搜索订单"
    @input="handleSearch"
  />
</template>

<script setup>
import { ref } from 'vue'
import { useDebounce } from '@/composables/useDebounce'

const searchKey = ref('')

const search = (value) => {
  console.log('搜索:', value)
  // 执行搜索逻辑
}

// 使用防抖,避免频繁请求
const handleSearch = useDebounce(search, 500)
</script>
// 性能优化5: 路由懒加载
// router/index.js - 优化前
import OrderList from '@/views/order/OrderList.vue'
import DishList from '@/views/dish/DishList.vue'

const routes = [
  { path: '/order', component: OrderList },
  { path: '/dish', component: DishList }
]

// router/index.js - 优化后(懒加载)
const routes = [
  {
    path: '/order',
    component: () => import(/* webpackChunkName: "order" */ '@/views/order/OrderList.vue')
  },
  {
    path: '/dish',
    component: () => import(/* webpackChunkName: "dish" */ '@/views/dish/DishList.vue')
  }
]
// 性能优化6: 请求缓存
// utils/request.js - 添加请求缓存
import axios from 'axios'

const cache = new Map()
const CACHE_TIME = 5 * 60 * 1000  // 5分钟缓存

const request = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  timeout: 10000
})

request.interceptors.request.use(config => {
  // 对GET请求启用缓存
  if (config.method === 'get' && config.cache !== false) {
    const cacheKey = config.url + JSON.stringify(config.params)
    const cached = cache.get(cacheKey)
    
    if (cached && Date.now() - cached.time < CACHE_TIME) {
      // 返回缓存数据
      config.adapter = () => {
        return Promise.resolve({
          data: cached.data,
          status: 200,
          statusText: 'OK (from cache)',
          headers: {},
          config
        })
      }
    }
  }
  
  return config
})

request.interceptors.response.use(response => {
  // 缓存GET请求的响应
  if (response.config.method === 'get' && response.config.cache !== false) {
    const cacheKey = response.config.url + JSON.stringify(response.config.params)
    cache.set(cacheKey, {
      data: response.data,
      time: Date.now()
    })
  }
  
  return response
})

export default request
posted @ 2025-12-03 20:00  清月明风  阅读(3)  评论(0)    收藏  举报