1. 项目背景与核心价值
校园外卖平台是当前高校生活服务数字化的重要切入点。随着高校扩招和封闭式管理趋势加强,传统食堂就餐模式暴露出三个显著痛点:高峰时段排队拥挤、特殊天气就餐不便、个性化需求难以满足。而主流商业外卖平台在校园场景中存在配送费用高、等待时间长、食品安全监管难等固有缺陷。
这个基于SpringBoot的校园外卖系统,正是针对这些痛点提出的本地化解决方案。与商业平台相比,它具有三个独特优势:
- 配送效率:由校内勤工俭学学生担任配送员,平均送达时间可控制在15分钟内
- 成本控制:免除外卖平台20%左右的抽成,商家让利5-8%给学生
- 安全闭环:所有商家经过学校资质审核,食品原料可追溯
从技术角度看,项目采用SpringBoot+Vue3前后端分离架构,完整实现了从用户下单到订单完成的闭环流程。我在开发过程中特别注重三个技术指标的优化:
- 订单创建响应时间<300ms
- 高峰期支持1000+并发请求
- 支付流程100%事务一致性保证
2. 技术架构设计
2.1 整体架构设计
系统采用经典的三层架构,但针对校园场景做了特殊优化:
code复制[前端层] Vue3 + Axios
↑↓ HTTP/HTTPS
[应用层] SpringBoot + JWT + MyBatis
↑↓ JDBC
[数据层] MySQL集群(主从复制)
创新设计点:
- 采用双MySQL实例部署,主库处理写操作,从库处理读操作
- 订单状态变更使用WebSocket实时推送,替代传统的轮询机制
- 敏感操作(如取消订单)引入二次确认和操作日志审计
2.2 数据库设计
核心表关系如下(简化版):
sql复制CREATE TABLE `order` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT NOT NULL COMMENT '关联user表',
`merchant_id` BIGINT NOT NULL COMMENT '关联merchant表',
`total_amount` DECIMAL(10,2) UNSIGNED NOT NULL,
`status` TINYINT NOT NULL COMMENT '0-待支付 1-已支付 2-已接单...',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_user` (`user_id`),
INDEX `idx_merchant` (`merchant_id`),
INDEX `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
设计要点:
- 所有金额字段使用DECIMAL(10,2)并添加UNSIGNED约束
- 状态字段使用TINYINT而非VARCHAR,节省存储空间
- 为高频查询字段建立复合索引
- 使用utf8mb4字符集支持emoji表情评价
2.3 安全设计
系统安全方案采用"三明治"防护策略:
- 传输层:全站HTTPS + HSTS头
- 应用层:
- 密码存储:BCrypt+盐值加密
- JWT Token设置2小时过期
- 接口防刷:Guava RateLimiter限流
- 数据层:
- SQL预编译防注入
- XSS过滤:Jsoup.clean()
- 敏感数据脱敏存储
3. 核心功能实现
3.1 订单状态机实现
订单状态流转是系统的核心逻辑,我们采用状态模式实现:
java复制public interface OrderState {
void pay(Order order);
void confirm(Order order);
void cancel(Order order);
// 其他操作...
}
@Component
@Scope("prototype")
public class PaidState implements OrderState {
@Override
public void confirm(Order order) {
order.setStatus(OrderStatus.CONFIRMED);
// 通知商家接单
websocketService.notifyMerchant(order);
}
@Override
public void cancel(Order order) {
if (System.currentTimeMillis() - order.getPayTime() > 30*60*1000) {
throw new BizException("支付超时不能取消");
}
order.setStatus(OrderStatus.CANCELLED);
// 退款逻辑
paymentService.refund(order);
}
}
状态转换规则:
- 待支付 → 已支付(支付成功)
- 已支付 → 已接单(商家操作)
- 已接单 → 配送中(商家操作)
- 配送中 → 已完成(用户确认)
- 任意状态 → 已取消(需符合条件)
3.2 高并发下单解决方案
针对中午12点的下单高峰,我们采用三级缓冲策略:
- 前端限流:提交按钮防重复点击(禁用+倒计时)
- 服务层:
- Redis分布式锁防超卖
- 库存预扣减(先扣缓存再异步同步DB)
- 数据层:
- 订单表分库分表(按用户ID哈希)
- 使用消息队列削峰填谷
关键代码示例:
java复制public Result createOrder(OrderDTO orderDTO) {
// 获取分布式锁
String lockKey = "order:" + orderDTO.getUserId();
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (!locked) {
return Result.error("操作太频繁");
}
try {
// 预扣减库存
Long remain = redisTemplate.opsForValue()
.decrement("stock:" + orderDTO.getGoodsId());
if (remain < 0) {
// 回滚
redisTemplate.opsForValue()
.increment("stock:" + orderDTO.getGoodsId());
return Result.error("库存不足");
}
// 异步落库
mqTemplate.convertAndSend("order.create", orderDTO);
return Result.success("下单成功");
} finally {
redisTemplate.delete(lockKey);
}
}
4. 典型问题解决方案
4.1 分布式事务问题
场景:支付成功后需要同时更新订单状态和扣除库存,如何保证一致性?
解决方案:
- 本地消息表+定时任务补偿
- 使用Seata AT模式(最终选择方案)
配置示例:
properties复制# application.properties
spring.cloud.alibaba.seata.tx-service-group=my_test_tx_group
seata.service.grouplist=127.0.0.1:8091
业务代码:
java复制@GlobalTransactional
public void handlePaySuccess(Long orderId) {
orderService.updateStatus(orderId, PAID);
inventoryService.deduct(orderId);
// 如果任意步骤失败,全部回滚
}
4.2 地理位置计算
需求:显示用户与商家的距离,并按距离排序
解决方案:
- 使用MySQL空间函数(需5.7+版本)
sql复制SELECT
id,
ST_Distance_Sphere(
POINT(116.404, 39.915),
POINT(longitude, latitude)
) AS distance
FROM merchant
ORDER BY distance
LIMIT 10;
- Redis GEO(性能更优)
java复制redisTemplate.opsForGeo().add("merchant:geo",
new Point(116.404, 39.915), "merchant_1");
redisTemplate.opsForGeo().distance("merchant:geo",
"user_loc", "merchant_1", Metrics.KILOMETERS);
5. 性能优化实践
5.1 缓存策略
采用多级缓存架构:
- 客户端缓存:静态资源CDN+本地Storage
- 服务端缓存:
- 热点数据:Redis(带本地Caffeine二级缓存)
- 列表数据:Redis List + 分页缓存
- 数据库缓存:
- 查询缓存(MySQL 8.0前)
- 缓冲池优化(innodb_buffer_pool_size)
缓存更新策略对比:
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Cache Aside | 实现简单 | 可能短暂不一致 | 读多写少 |
| Write Through | 强一致 | 性能较差 | 写多读少 |
| Write Behind | 性能最好 | 可能丢数据 | 允许异步 |
5.2 SQL优化案例
问题:订单查询页响应慢(>2s)
优化前:
sql复制SELECT * FROM orders
WHERE user_id = 123
ORDER BY create_time DESC;
优化措施:
- 添加复合索引:(user_id, create_time)
- 改写查询:
sql复制SELECT id, order_no, status, amount
FROM orders
WHERE user_id = 123
ORDER BY create_time DESC
LIMIT 10;
效果:查询时间从2100ms降至35ms
6. 部署方案
6.1 容器化部署
使用Docker Compose编排服务:
yaml复制version: '3'
services:
mysql-master:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
ports:
- "3306:3306"
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/conf:/etc/mysql/conf.d
redis:
image: redis:6
ports:
- "6379:6379"
command: redis-server --requirepass ${REDIS_PASS}
app:
build: .
ports:
- "8080:8080"
depends_on:
- mysql-master
- redis
environment:
- SPRING_PROFILES_ACTIVE=prod
6.2 监控方案
Prometheus + Grafana监控体系:
- JVM监控:Micrometer
- 业务指标:自定义Counter/Gauge
- 告警规则:
- 订单创建失败率>1%
- 平均响应时间>500ms
- JVM内存使用>80%
配置示例:
java复制@Bean
MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config()
.commonTags("application", "campus-food");
}
7. 踩坑经验
7.1 分布式ID生成
问题:订单ID使用数据库自增,分库后出现重复
解决方案:
- 雪花算法(最终采用)
java复制public class SnowflakeIdGenerator {
private final long datacenterId;
private final long machineId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨");
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & 0xFFF;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - 1288834974657L) << 22)
| (datacenterId << 17)
| (machineId << 12)
| sequence;
}
}
7.2 缓存穿透
现象:大量请求查询不存在的商品ID,导致DB压力大
解决方案:
- 布隆过滤器前置校验
- 缓存空值(设置短TTL)
实现代码:
java复制public Product getProduct(Long id) {
// 布隆过滤器判断
if (!bloomFilter.mightContain(id)) {
return null;
}
// 查询缓存
String key = "product:" + id;
Product product = redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
// 查询数据库
product = productMapper.selectById(id);
if (product == null) {
// 缓存空值
redisTemplate.opsForValue().set(key, "", 5, TimeUnit.MINUTES);
return null;
}
// 写入缓存
redisTemplate.opsForValue().set(key, product, 1, TimeUnit.HOURS);
return product;
}
8. 扩展方向
- 智能调度:结合课程表数据预测订单高峰,提前调配配送资源
- 营养分析:基于菜品成分数据提供健康建议
- 无人配送:对接校园内的无人车配送系统
- 虚拟餐厅:允许学生自主创建"虚拟餐厅",开展特色餐饮服务
在实际开发中,最大的体会是:校园场景的技术方案必须兼顾先进性和实用性。比如我们曾考虑引入Kafka处理订单消息,但最终选择了RabbitMQ,就是因为其更符合团队现有技术栈,且校园场景的订单量尚未需要Kafka级别的吞吐能力
