1. 项目背景与需求分析
大学食堂作为校园生活的重要场景,传统运营模式正面临诸多挑战。每到中午11:30的下课高峰,我们总能看到这样的场景:每个档口前蜿蜒的长队、手写菜单的频繁涂改、收银员手忙脚乱地计算金额。这种模式不仅让学生浪费大量时间排队,也给餐厅管理带来巨大压力。
我去年参与开发的某高校食堂数字化改造项目,通过实地调研发现了几个核心痛点:
- 高峰时段平均排队时间达25分钟
- 人工结算错误率约3%
- 30%的学生因等待时间过长选择外卖
- 食材浪费率高达18%
基于这些发现,我们决定采用SpringBoot框架开发点餐管理系统。选择SpringBoot主要基于三个考量:首先,其内嵌Tomcat和自动配置特性可以快速搭建服务;其次,丰富的Starter依赖能轻松整合安全、数据库等组件;最重要的是,其优雅的DSL设计让团队中的学生开发者也能快速上手。
2. 技术架构设计
2.1 整体架构方案
系统采用经典的三层架构,但在细节上做了针对性优化:
code复制表示层:Vue3 + Element Plus(PC管理端) + UniApp(微信小程序)
业务层:SpringBoot 2.7 + Spring Security
数据层:MySQL 8.0(主业务) + Redis 7.0(缓存)
特别在通信设计上,我们没有采用常规的HTTP轮询,而是为订单状态更新引入了WebSocket长连接。实测显示,这种方式将订单状态同步延迟从平均3秒降低到300毫秒以内。
2.2 数据库设计精要
核心表结构设计中,有几个关键决策值得说明:
- 订单表使用BIGINT主键而非UUID,既保证索引效率又避免分布式ID复杂度
- 金额字段统一采用DECIMAL(10,2)并搭配@Digits注解校验
- 建立复合索引(idx_user_status)加速订单查询:
sql复制CREATE INDEX idx_user_status ON orders(user_id, status);
在关系建模时,我们特别注意了级联操作的控制。比如订单与订单项是1:N关系,但配置的是cascade = CascadeType.PERSIST而非ALL,避免误删风险。
3. 核心功能实现
3.1 高并发下单流程
订单服务是系统核心,我们采用"预扣库存→创建订单→支付验证"的三段式处理。关键代码示例:
java复制@Transactional
public OrderResult createOrder(OrderRequest request) {
// 1. 库存预扣减
List<DishStock> lockResults = dishMapper.batchLockStock(
request.getItems().stream()
.collect(Collectors.toMap(
OrderItemRequest::getDishId,
OrderItemRequest::getQuantity)));
// 2. 订单创建
Order order = assembleOrder(request);
orderMapper.insert(order);
// 3. 订单项处理
List<OrderItem> items = assembleItems(request, order.getId());
orderItemMapper.batchInsert(items);
return new OrderResult(order.getId(), OrderStatus.CREATED);
}
这里有几个优化点:
- 使用batch操作减少数据库交互
- 库存预扣减采用乐观锁避免超卖
- 事务注解保证原子性
3.2 支付状态机设计
支付流程我们实现了状态机模式,明确定义了状态转换规则:
java复制public enum PaymentStatus {
UNPAID {
@Override
public void pay(PaymentContext context) {
if (context.verifyPayment()) {
context.changeStatus(PAID);
context.notifyOrderService();
}
}
},
PAID {
@Override
public void refund(PaymentContext context) {
// 退款逻辑
}
};
public abstract void pay(PaymentContext context);
public void refund(PaymentContext context) {
throw new IllegalStateException();
}
}
这种设计将复杂的流程判断转化为状态对象的行为,使代码更易维护。在日均3000+订单的实际运行中,支付模块始终保持零状态异常。
4. 性能优化实践
4.1 缓存策略
针对高频访问的菜单数据,我们设计了二级缓存:
- 本地Caffeine缓存(有效期2分钟)
- Redis集群缓存(有效期10分钟)
缓存更新采用"先删后更"策略,并通过@CacheEvict注解保证一致性:
java复制@CacheEvict(value = "menus", key = "#dish.category")
public void updateDish(Dish dish) {
dishMapper.updateById(dish);
eventPublisher.publishEvent(new MenuUpdateEvent(dish.getId()));
}
4.2 SQL优化案例
在订单查询接口中,我们发现分页查询随着数据量增加明显变慢。通过EXPLAIN分析发现全表扫描问题,最终优化方案:
sql复制-- 原查询(执行时间1200ms)
SELECT * FROM orders WHERE user_id = ? ORDER BY create_time DESC LIMIT 10 OFFSET 20;
-- 优化后(执行时间80ms)
SELECT * FROM orders
WHERE user_id = ? AND create_time < ?
ORDER BY create_time DESC LIMIT 10;
关键改进点:
- 用时间戳替代OFFSET分页
- 建立(user_id, create_time)复合索引
- 使用覆盖索引减少回表
5. 安全防护体系
5.1 认证授权设计
采用JWT + Spring Security的方案,但做了几点增强:
- 双Token机制(accessToken 30分钟 + refreshToken 7天)
- 指纹校验防止令牌劫持
- 接口级权限控制:
java复制@PreAuthorize("hasRole('STUDENT') or hasRole('STAFF')")
@GetMapping("/orders/{id}")
public OrderDetail getOrderDetail(@PathVariable Long id) {
// ...
}
5.2 防刷策略
针对下单接口我们实现了多维度防护:
- 滑动窗口限流(Redis + Lua实现)
- 设备指纹识别
- 业务规则限制(如单日下单次数)
核心限流逻辑:
java复制public boolean tryAcquire(String key, int limit, int timeout) {
String luaScript = "local current = redis.call('GET', KEYS[1])...";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(key),
limit, timeout);
return result == 1;
}
6. 部署与监控
6.1 容器化部署
采用Docker Compose编排服务,关键配置包括:
- 资源限制(Java进程配置-XX:+UseContainerSupport)
- 健康检查接口
- 日志收集卷挂载
yaml复制services:
app:
image: campus-food:1.0
deploy:
resources:
limits:
cpus: '2'
memory: 2G
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
6.2 监控方案
基于Prometheus + Grafana搭建监控看板,重点监控:
- JVM内存使用(特别是GC情况)
- 接口P99响应时间
- 数据库连接池使用率
- 订单创建成功率
配置示例:
properties复制management.endpoints.web.exposure.include=*
management.metrics.export.prometheus.enabled=true
7. 踩坑与经验
7.1 事务失效场景
在开发支付回调处理时,我们遇到事务不生效的问题。最终发现是因为:
- 自调用导致@Transactional失效
- 异常类型配置错误(默认只回滚RuntimeException)
修正方案:
java复制@Transactional(rollbackFor = Exception.class)
public void handlePaymentNotify(PaymentNotify notify) {
paymentService.processPayment(notify); // 通过代理对象调用
}
7.2 日期处理陷阱
初期直接使用LocalDateTime存储时间,导致前端显示时区错乱。解决方案:
- 数据库统一UTC时间
- 返回给前端时明确时区信息:
java复制@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
8. 扩展功能实现
8.1 智能推荐算法
基于用户历史订单,实现简单的协同过滤推荐:
java复制public List<Dish> recommendDishes(Long userId) {
// 1. 获取相似用户
List<Long> similarUsers = findSimilarUsers(userId);
// 2. 提取热门菜品
return dishMapper.selectPopularInGroups(
similarUsers,
LocalDateTime.now().getHour());
}
8.2 数据统计分析
利用Spring Batch实现每日营业统计:
java复制@Scheduled(cron = "0 0 3 * * ?")
public void generateDailyReport() {
jobLauncher.run(dailyReportJob, new JobParametersBuilder()
.addDate("execDate", new Date())
.toJobParameters());
}
报表包含:
- 各时段订单分布
- 菜品销售排行
- 支付方式占比
这个项目让我深刻体会到,一个好的校园系统不仅需要技术深度,更要理解真实的用餐场景。比如我们在第三周才发现,中午12:05-12:15会出现瞬时高峰,这促使我们重新设计了限流策略。技术永远是为业务服务的,这或许是这个项目给我的最大启示。