第六天项目
苍穹外卖项目 - 第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
浙公网安备 33010602011771号