Java毕业设计实战:基于Spring Boot的外卖系统开发与论文撰写指南
关键词:java毕业设计 外卖加论文、Spring Boot、MyBatis、Clean Code、论文框架
一、背景痛点:为什么你的外卖系统总被导师打回?
做毕业设计最怕“一看就会,一写就废”。我帮导师审了三年本科项目,发现外卖系统高频踩坑集中在下面几点:
- 业务逻辑一锅粥:把“下单”写成一条巨长 SQL,订单、库存、优惠券全塞一起,后期连自己都看不懂。
- 技术栈拍脑袋:听说微服务火就硬拆 5 个模块,结果 RPC 调不通,答辩现场直接翻车。
- 代码结构放飞:Controller 里写业务,Service 里写 SQL,一个类 2000 行,查重率 60% 起步。
- 论文和代码脱节:正文贴 3 张截图就算“系统设计”,老师一问“事务在哪”就支支吾吾。
如果你也中枪,别急,下面这套“Spring Boot + 单体 + 模块化”打法,就是给新手量身定制的“保命”方案。
二、技术选型:Spring Boot 为何是毕业设计最优解?
先放结论:本科阶段,Spring Boot 单体 > SSM > 微服务。原因一张表看懂:
| 维度 | SSM(XML 版) | Spring Boot 单体 | Spring Cloud 微服务 |
|---|---|---|---|
| 配置量 | 高 | 极低 | 爆炸 |
| 学习曲线 | 中 | 低 | 高 |
| 开发周期 | 3-4 周 | 1-2 周 | 6 周起步 |
| 答辩风险 | 低 | 低 | 极高(易挂) |
| 代码查重 | 高(模板老) | 中 | 低但复杂 |
对新手而言,Spring Boot 把 Tomcat、MyBatis、日志、监控全部“一键依赖”,让你把有限时间花在“业务 + 论文”上,而不是调 XML。微服务不是不能做,而是做了后,你要解释“为什么订单服务调用用户服务超时 3 次”——本科答辩现场没人想听分布式事务。
三、核心实现:三大模块拆给你看
项目结构先统一,后面所有代码都按这套路径:
src/main/java/com/takeaway ├── common // 工具、常量、全局异常 ├── config // JWT、Redis、MyBatis ├── controller // 仅路由与参数校验 ├── service // 业务,含接口与实现 ├── mapper // MyBatis 接口 ├── entity // DO、DTO、VO 分层 └── job // 定时任务(后面留作业)下面按“用户登录→购物车→订单生成”顺序,讲新手最容易懵的三段逻辑。
1. 用户登录:JWT + Redis 双缓存
痛点:Session 在集群下失效,Redis 当缓存又当数据库,键名乱。
方案:
- 登录成功后生成 JWT(过期 30 min),同时把 userId 作为 key 存 Redis,value 存用户基本信息,TTL 同样 30 min。
- 每次请求带 Header
Authorization: Bearer <token>,拦截器解析 JWT,若 Redis 里 key 不存在则拒绝。 - 刷新令牌:写个
/refresh接口,旧 JWT 未过期即可换新 JWT,同时延长 Redis TTL,实现“滑动窗口”。
代码片段(Clean Code:方法不超过 20 行,命名自解释):
@Component @RequiredArgsConstructor public class JwtOperator { private final RedisTemplate<String,Object> redisTemplate; private final StringSecretKey secretKey; // 注入 yml 里的密钥 public String createJwt(Long userId){ returnToken token = Jwts.builder() .setSubject("user") .claim("userId", userId) .setExpiration(Date.from(Instant.now().plus(30, ChronoUnit.MINUTES))) .signWith(secretKey) .compact(); redisTemplate.opsForValue().set("jwt:user:" + userId, token, 30, TimeUnit.MINUTES); return token; } public boolean validate(String token){ try { Claims body = Jwts.parserBuilder().setSigningKey(secretKey).build() .parseClaimsJws(token).getBody(); Long userId = body.get("userId", Long.class); return redisTemplate.hasKey("jwt:user:" + userId); } catch (JwtException | IllegalArgumentException e) { return false; } } }2. 购物车:先写接口幂等,再谈性能
痛点:刷新页面数量 +1,重复点击“加入购物车”导致同商品多条记录。
方案:
- 接口幂等:前端生成 UUID 作为
clientId,后端用userId + skuId + clientId做唯一索引,重复请求直接返回“已添加”。 - 缓存:购物车读多写少,Redis Hash 结构
cart:{userId},field=skuId,value=数量 + 更新时间,减轻 DB 压力。 - 事务:下单扣库存时,先删缓存再写 DB,防止“缓存脏读”。
3. 订单生成:本地事务 + 乐观锁
痛点:多人同时下单,库存超卖;订单表、订单明细、库存、优惠券四张表不同步。
方案:
- Spring 本地事务:
@Transactional(rollbackFor = Exception.class),统一入口在 Service 实现层。 - 乐观锁:给
product表加version字段,更新库存时set stock = stock - ?, version = version + 1 where version = ? and stock > 0,返回影响行数 0 抛异常,提示“库存不足”。 - 订单号生成:采用雪花算法,保证全局唯一,避免 UUID 过长被导师吐槽。
- 事务顺序:先写订单主表→订单明细→扣库存→清购物车→标记优惠券已用,任何一步失败全部回滚。
关键代码(仅保留事务骨架,细节去 GitHub 自取):
@Service @RequiredArgsConstructor public class OrderServiceImpl implements OrderService { private final ProductMapper productMapper; private final OrderMapper orderMapper; private final CartService cartService; @Override @Transactional(rollbackFor = Exception.class) public Long createOrder(Long userId, OrderDTO dto){ // 1. 参数校验略 // 2. 雪花算法生成订单号 String orderNo = IdUtil.getSnowflakeNextIdStr(); // 3. 写订单主表 Order order = Order.builder() .orderNo(orderNo) .userId(userId) .status(OrderStatusEnum.PENDING_PAYMENT) .build(); orderMapper.insert(order); // 4. 批量写订单明细 + 扣库存 dto.getItems().forEach(item -> { int affected = productMapper.decreaseStock(item.getSkuId(), item.getQuantity()); if (affected == 0) { throw new BizException("库存不足"); } orderMapper.insertItem(order.getId(), item); }); // 5. 清购物车 cartService.clear(userId); return order.getId(); } }四、Clean Code 示例:JWT 工具类 + Redis 缓存菜品
上面 JWT 已经示范,这里再补一段“缓存菜品”的 Service,展示如何“让代码自己说话”。
@Service @RequiredArgsConstructor public class MenuService { private final RedisTemplate<String,Object> redisTemplate; private final ProductMapper productMapper; private static final String MENU_CACHE_KEY = "menu:category:"; /** * 查询菜品列表,先读缓存,没有再写回 */ public List<ProductVO> listByCategory(Integer categoryId){ String key = MENU_CACHE_KEY + categoryId; List<ProductVO> cached = (List<ProductVO>) redisTemplate.opsForValue().get(key); if (cached != null){ return cached; } List<ProductVO> dbList = productMapper.selectByCategory(categoryId); if (!dbList.isEmpty()){ redisTemplate.opsForValue().set(key, dbList, 10, TimeUnit.MINUTES); } return dbList; } /** * 后台更新菜品时,删除缓存,保证一致性 */ public void updateProduct(ProductForm form){ productMapper.update(form); redisTemplate.delete(MENU_CACHE_KEY + form.getCategoryId()); } }要点:
- 方法名动词开头,参数名统一。
- 魔法值放常量,缓存 key 带业务前缀。
- 不写一行注释也能看懂,但复杂算法仍需注释“为什么”而不是“做什么”。
五、性能与安全:别让“小项目”成为老师口中的反面教材
数据库索引
- 订单表
user_id+status联合索引,解决“我的订单”列表慢。 - 订单明细
order_id单列索引,避免子查询全表扫描。 - 商品表
version字段也加索引,乐观锁更新时减少行锁范围。
- 订单表
SQL 注入
- MyBatis 用
#{}占位符,拒绝${}拼接。 - 额外提供 PageHelper 分页,自己写
limit一定用PageParam对象封装,防止limit #{offset},#{size}被传参恶意放大。
- MyBatis 用
密码加密
- 密码字段
char(60),Spring Security 的BCryptPasswordEncoder,强度 10 足够,存库自带盐。 - 登录接口限流:Redis 计数 5 次/分钟,超限锁定 10 分钟,毕业设计也能讲“防暴力破解”。
- 密码字段
六、生产环境避坑:从本地到答辩的最后一公里
本地部署常见问题
- MySQL 8 时区报错:url 加
serverTimezone=Asia/Shanghai。 - Redis 连接拒绝:Linux 云服务器需改
redis.conf把bind 127.0.0.1注释掉,再关保护模式。 - 端口未放行:阿里云/腾讯云安全组开放 8080、3306、6379,别等演示现场才发现连不上。
- MySQL 8 时区报错:url 加
论文查重规避技巧
- 系统结构图自己画,别抄 CSDN。
- 技术介绍先讲“官方定义”,再写“自己的理解”,查重系统对“口语化解释”识别度低。
- 代码段只放核心 15 行,剩余放 GitHub 链接,正文引用“见附录 A”。
功能演示视频录制
- 用 OBS 1080P/30 帧,先走正向流程:注册→登录→加购物车→下单→支付(可 mock)。
- 再演示异常:库存不足、重复登录、订单超时取消(作业见下)。
- 录完剪成 3 分钟,加字幕,老师不想看 10 分钟长镜头。
七、结课作业:给订单加上“超时 30 分钟未支付自动取消”
需求很简单,实现方式至少三种:
- Spring 的
@Scheduled轮询数据库,逻辑简单但 DB 压力大。 - Redis 键空间通知,下单时给
order:{orderNo}设置 30 min TTL,过期后 Redis 推事件,Java 监听取消订单。 - RabbitMQ 死信队列,下单发消息,30 min 后自动路由到“取消”队列,消费端处理。
建议:先写第 1 种保毕业,再写第 2 种当亮点,论文里对比三方案优缺点,老师一看就知道你“研究过”。
最后的小提醒:毕业设计不是“写代码”,而是“讲清楚你为什么这么写”。把上面的模块跑通,再把“事务、幂等、缓存、安全”四点写进论文,答辩时自信一点,老师还没开口你就赢了。祝你一次通过,早日上岸!