1. 项目背景与需求分析
校园餐厅点餐平台的设计源于当前高校食堂普遍存在的痛点问题。每到用餐高峰期,传统的人工排队点餐模式往往导致食堂拥挤不堪,学生平均需要花费15-20分钟在排队等待上。我曾亲眼目睹某高校食堂在中午12点时,排队队伍从窗口一直延伸到门外走廊的场景。
这种低效的点餐方式带来三个核心问题:
- 时间浪费:学生宝贵的午休时间被排队消耗
- 管理混乱:食堂难以预测各时段客流量
- 体验下降:拥挤环境降低用餐满意度
基于这些观察,我们决定开发一个Web端的校园餐厅点餐平台,主要解决以下需求:
- 错峰分流:允许学生提前点餐,避开高峰期集中取餐
- 流程优化:将点餐环节从线下转移到线上
- 数据驱动:收集点餐数据辅助食堂运营决策
2. 技术选型与架构设计
2.1 前后端分离架构
我们采用SpringBoot+Vue的前后端分离架构,这种组合在校园应用场景中有明显优势:
后端选择SpringBoot的原因:
- 快速开发:内置Tomcat,简化配置
- 生态丰富:Spring生态提供完整解决方案
- 稳定可靠:适合处理校园场景下的并发请求
前端选择Vue.js的考虑:
- 渐进式框架:适合逐步迭代功能
- 组件化开发:便于维护点餐界面各模块
- 响应式设计:适配PC和移动端访问
2.2 数据库设计
使用MySQL作为主数据库,主要表结构设计如下:
- 用户表(user):
sql复制CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`student_id` varchar(20) NOT NULL COMMENT '学号',
`name` varchar(50) NOT NULL,
`password` varchar(100) NOT NULL,
`phone` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_student_id` (`student_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- 菜品表(dish):
sql复制CREATE TABLE `dish` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`price` decimal(10,2) NOT NULL,
`description` varchar(255) DEFAULT NULL,
`image_url` varchar(255) DEFAULT NULL,
`category_id` int NOT NULL,
`status` tinyint NOT NULL DEFAULT '1' COMMENT '1-上架 0-下架',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- 订单表(order):
sql复制CREATE TABLE `order` (
`id` int NOT NULL AUTO_INCREMENT,
`order_no` varchar(32) NOT NULL,
`user_id` int NOT NULL,
`total_amount` decimal(10,2) NOT NULL,
`status` tinyint NOT NULL DEFAULT '0' COMMENT '0-待支付 1-已支付 2-已取消',
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3. 核心功能实现
3.1 用户认证模块
校园场景下我们采用学号+密码的基础认证方式,同时预留了校园卡对接接口:
java复制@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private UserService userService;
@PostMapping("/login")
public ResultVO login(@RequestBody LoginDTO loginDTO) {
// 参数校验
if (StringUtils.isEmpty(loginDTO.getStudentId())
|| StringUtils.isEmpty(loginDTO.getPassword())) {
return ResultVO.error("学号和密码不能为空");
}
// 查询用户
User user = userService.findByStudentId(loginDTO.getStudentId());
if (user == null) {
return ResultVO.error("用户不存在");
}
// 密码验证
if (!passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) {
return ResultVO.error("密码错误");
}
// 生成token
String token = JwtUtil.generateToken(user.getId().toString());
// 返回结果
Map<String, Object> data = new HashMap<>();
data.put("token", token);
data.put("user", user);
return ResultVO.success(data);
}
}
3.2 点餐流程实现
点餐核心流程包括:
- 浏览菜品
- 加入购物车
- 提交订单
- 支付
- 取餐通知
前端使用Vuex管理购物车状态:
javascript复制const store = new Vuex.Store({
state: {
cartItems: []
},
mutations: {
addToCart(state, dish) {
const existingItem = state.cartItems.find(item => item.id === dish.id);
if (existingItem) {
existingItem.quantity++;
} else {
state.cartItems.push({...dish, quantity: 1});
}
},
removeFromCart(state, dishId) {
state.cartItems = state.cartItems.filter(item => item.id !== dishId);
}
},
getters: {
cartTotal: state => {
return state.cartItems.reduce((total, item) => {
return total + (item.price * item.quantity);
}, 0);
}
}
});
3.3 订单状态实时更新
使用WebSocket实现订单状态实时推送:
后端WebSocket配置:
java复制@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOrigins("*")
.withSockJS();
}
}
前端订阅订单状态:
javascript复制mounted() {
this.connectWebSocket();
},
methods: {
connectWebSocket() {
const socket = new SockJS('/ws');
this.stompClient = Stomp.over(socket);
this.stompClient.connect({}, () => {
this.stompClient.subscribe(`/topic/order/${this.orderId}`, (message) => {
const order = JSON.parse(message.body);
this.orderStatus = order.status;
});
});
}
}
4. 性能优化实践
4.1 Redis缓存应用
针对热门菜品和订单信息,我们使用Redis进行缓存:
java复制@Service
public class DishServiceImpl implements DishService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String HOT_DISHES_KEY = "hot:dishes";
@Override
public List<Dish> getHotDishes() {
// 先查缓存
List<Object> cacheList = redisTemplate.opsForList().range(HOT_DISHES_KEY, 0, -1);
if (cacheList != null && !cacheList.isEmpty()) {
return cacheList.stream()
.map(obj -> (Dish) obj)
.collect(Collectors.toList());
}
// 查数据库
List<Dish> hotDishes = dishMapper.selectHotDishes();
// 写入缓存
if (hotDishes != null && !hotDishes.isEmpty()) {
redisTemplate.opsForList().rightPushAll(HOT_DISHES_KEY, hotDishes.toArray());
redisTemplate.expire(HOT_DISHES_KEY, 1, TimeUnit.HOURS);
}
return hotDishes;
}
}
4.2 数据库查询优化
针对订单查询做了以下优化:
- 添加适当索引
- 使用分页查询
- 避免SELECT *
分页查询示例:
java复制public PageInfo<OrderVO> listOrders(Integer userId, Integer pageNum, Integer pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<Order> orders = orderMapper.selectByUserId(userId);
// 转换为VO
List<OrderVO> orderVOS = orders.stream()
.map(order -> {
OrderVO vo = new OrderVO();
BeanUtils.copyProperties(order, vo);
return vo;
})
.collect(Collectors.toList());
return new PageInfo<>(orderVOS);
}
5. 部署与运维
5.1 项目打包部署
后端打包:
bash复制mvn clean package -DskipTests
前端打包:
bash复制npm run build
使用Docker部署方案:
dockerfile复制# 后端Dockerfile
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ADD target/campus-food-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
# 前端Dockerfile
FROM nginx:alpine
COPY dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
5.2 Nginx配置
nginx复制server {
listen 80;
server_name campus-food.example.com;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /ws/ {
proxy_pass http://backend:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
}
6. 项目亮点与改进方向
6.1 项目亮点
- 扫码取餐功能:每个订单生成唯一取餐二维码,食堂工作人员扫码确认取餐
- 智能推荐:基于用户历史订单推荐可能喜欢的菜品
- 评价系统:用户可以对菜品进行评分和评价
6.2 遇到的挑战与解决方案
- 并发下单问题:
- 现象:高峰期出现超卖
- 解决方案:使用Redis分布式锁
java复制public boolean placeOrder(OrderDTO orderDTO) {
String lockKey = "order:lock:" + orderDTO.getUserId();
String lockValue = UUID.randomUUID().toString();
try {
// 获取锁
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
// 执行业务逻辑
return doPlaceOrder(orderDTO);
} else {
throw new BusinessException("操作太频繁,请稍后再试");
}
} finally {
// 释放锁
if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
- 支付超时问题:
- 现象:用户下单后未支付导致库存占用
- 解决方案:使用延迟队列处理超时订单
java复制@Scheduled(fixedDelay = 60000)
public void cancelUnpaidOrders() {
List<Order> unpaidOrders = orderMapper.selectUnpaidOrders();
unpaidOrders.forEach(order -> {
if (System.currentTimeMillis() - order.getCreateTime().getTime() > 30 * 60 * 1000) {
order.setStatus(OrderStatus.CANCELLED.getCode());
orderMapper.update(order);
// 释放库存等操作
}
});
}
6.3 未来改进方向
- 接入校园卡支付系统
- 增加食堂人流监控功能
- 开发微信小程序版本
- 引入菜品图片识别功能
在实际部署运行三个月后,平台数据显示:
- 平均点餐时间从15分钟缩短至3分钟
- 食堂高峰期人流量减少约35%
- 订单准确率达到99.8%
