ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Java+SpringBoot的菜品销售系统实战:从需求分析到部署答辩全攻略

基于Java+SpringBoot的菜品销售系统实战:从需求分析到部署答辩全攻略

第一次拿到这个题目的时候,说实话我是有点小看它的。基于Java+SpringBoot的菜品销售系统,听上去就是CRUD,无非菜品表、订单表、用户表,再套几个页面,似乎一晚上就能写完。但真正把需求理清、把代码写顺、把整个流程跑通之后,我不得不改观——它麻雀虽小五脏俱全,几乎覆盖了SpringBoot后端开发的大部分核心知识点,加上源码、论文(LW)、调试文档和讲解演示这一整套交付物,做为一个毕业设计或者入门练手项目,性价比高得离谱。

这篇文章我不打算做成"项目说明书"式的复述,而是换个角度:如果现在让你独立把这个系统从零搭出来,从需求拆分、表结构设计、技术选型,到实际编码时容易踩的坑,再到最后论文和答辩怎么准备,每一步该怎么想、怎么做。我会把我实际调试过程中遇到的那些问题、查了很久才找到原因的细节,全部摊开来讲,希望能帮你少走弯路。

1. 这个题目比想象中能挖的内容多得多:需求分析与功能定位

1.1 别急着写代码,先把"卖菜"这件事的角色和流程理清楚

很多人拿到类似题目,第一反应是打开IDE直接建工程。这是大忌。菜品销售系统听起来简单,但它本质上是一个完整的小型电商系统,涉及到"用户端"和"管理端"两个完全不同的操作场景,两边的诉求差别很大。

先捋一下角色:

  • 普通用户/顾客:注册登录、浏览菜品分类、查看菜品详情、加入购物车、提交订单、查看自己的订单列表和状态。
  • 管理员:登录后台、管理菜品分类、上下架菜品、维护菜品图片和价格、处理订单状态(比如确认接单、完成订单)、查看基本的统计数据。

这两个角色不是简单的"多几个接口"的区别,而是整个设计思路的分叉。用户端讲究的是操作顺畅、信息展示清晰;管理端讲究的是信息密度高、操作高效。所以做需求分析的时候,我会建议先把这个系统拆成前台和后台两条线,分别列出功能点,再去重、合并同类项。

以这个题目为例,最终的功能地图大致是这样的:

模块用户端功能管理端功能
用户管理注册、登录、个人信息维护用户列表查看(可选)
菜品分类按分类浏览菜品分类增删改、排序
菜品管理菜品列表、详情、图片、销量排序菜品增删改、上下架、库存设置
购物车加入购物车、修改数量、删除、合计金额无
订单管理创建订单、查看订单、取消订单查看全部订单、订单状态流转
统计模块无菜品销量统计、订单数量趋势(可选)

这个表看起来简单,但它是后面所有工作的依据。功能点定下来之后,才能谈表结构、接口设计、页面划分。否则写着写着就会开始"加功能",最后项目越做越臃肿,论文和代码都对不上。

1.2 功能清单怎么定:从菜单到订单的状态流转拆解

如果你只会新增、删除、修改、查询这四个操作,那这个系统确实没什么好写的。但订单系统最核心的东西不是CRUD,而是状态流转。

订单从用户提交开始,至少要经历这几个状态:待支付/待接单 -> 已接单/制作中 -> 已完成 -> 已取消。有些系统还会加"配送中""退款中"之类的状态,但毕设不建议搞太复杂,会把状态机搞乱。

我推荐用一个状态字段来表示,具体取值在代码里定义成枚举或者常量:

public enum OrderStatus { PENDING(0, "待接单"), PROCESSING(1, "制作中"), COMPLETED(2, "已完成"), CANCELED(3, "已取消"); }

每个状态之间的跳转必须做校验,比如已取消的订单不能再变成已完成,已完成的订单不能又被恢复为制作中。这些规则在Service层统一校验,不要散落在Controller里。

状态流转想清楚之后,很多隐藏的功能点就浮现出来了:

  • 用户取消订单的时机限制(比如管理员接单之后就不允许取消了)。
  • 支付环节如果做不了真实支付,可以做模拟支付,或者直接省略为"提交即待接单"。
  • 菜品下架之后,购物车里已有的该菜品怎么办?是下单时校验,还是在购物车中直接移除?

这些问题看起来琐碎,但都是答辩时老师喜欢追问的点。提前想好,写代码的时候顺手处理,比答辩时支支吾吾强一百倍。

2. 技术栈不是越新越好:SpringBoot项目选型背后的真实考量

2.1 为什么是SpringBoot而不是SSH、SSM,更不是微服务

这个问题听起来老生常谈,但我是认真的。我做过的毕设指导里,真有学生一上来就问我"要不要用Spring Cloud"。我反问他:你的系统里需要几个服务?他说一个。那为什么要用Spring Cloud?他答不上来。

技术选型的核心原则是让代码服务于需求,而不是让需求迁就技术。菜品销售系统的用户量和数据量根本达不到需要分布式、需要消息队列的程度。用SpringBoot单体应用,部署简单、调试方便、出问题好排查,这就够了。

SpringBoot在这个项目里的优势其实不在于"快",而在于约定大于配置,省去了一堆XML配置的重复劳动。同时它的生态太成熟了,整合MyBatis、Redis、JWT、文件上传,几乎都有现成的starter,官方文档也足够详细。对一个需要快速出活、还要讲清楚的毕设项目来说,这是最稳妥的选择。

版本选择上,如果Java基础是8,建议用SpringBoot 2.7.x,稳定且资料多;如果装了Java 17,可以直接上SpringBoot 3.x。但要注意,SpringBoot 3.x要求Jakarta命名空间,很多老教程里javax.servlet开头的代码会报错,查问题的时候容易懵。所以如果不是特别追求新,我建议2.7.x就够用了。

2.2 前端到底用Vue还是模板引擎:取决于你要演示的还是完整的产品

这是另一个高频纠结。有的同学用Thymeleaf服务端渲染,一封不到一百行的HTML里写了半个项目的逻辑;有的同学直接上前后端分离,Vue + Element UI + Axios,一份接口文档几十页。

我的看法是:如果时间和精力允许,尽量用前后端分离。原因很简单:答辩演示的时候,Vue + Element UI做出来的页面效果,比服务端渲染的裸页面好看太多了,这是印象分。更重要的是,前后端分离能让你在论文里多写一章"前后端交互设计与接口规范",内容量直接拉满。

但前后端分离也有代价,最大的问题是环境安装和部署。你得装Node、装npm、跑开发服务器,最后还得处理跨域问题。这些坑在下面第4章我会专门讲。如果前端基础实在薄弱,退而求其次用Thymeleaf也没问题,课题名依然是"SpringBoot菜品销售系统",不算跑题。

2.3 数据库和ORM的选择:MySQL + MyBatis-Plus的实用主义

数据库方面,MySQL是绝对的主流,没有悬念。需要注意的只是版本(建议5.7或8.0)和字符集(建议utf8mb4,否则部分生僻菜品名或表情符号会乱码)。

ORM方面我比较推荐MyBatis-Plus,而不是原生MyBatis或者Spring Data JPA。

理由如下:

  • MyBatis-Plus的单表CRUD基本上不用写SQL,内置的BaseMapper接口直接搞定,省时间。
  • 条件构造器非常灵活,比如"查询销量前10的菜品""按分类ID和上架状态过滤"这类需求,用LambdaQueryWrapper几行代码就写完。
  • 分页插件在MyBatis-Plus里只需要配置一个拦截器,联表查询照常写XML,完全不受影响。
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency>

注意MyBatis-Plus版本选择的时候看一眼跟SpringBoot版本的兼容性,不要盲目下载最新版。3.5.x在2.x和3.x的SpringBoot下都有对应兼容包,实测比较稳。

3. 落地过程中的核心模块:表结构设计、权限控制和订单状态机

3.1 五张核心表是怎么设计出来的

表结构设计是整个系统的骨架,骨架歪了,后面怎么补都难受。我的习惯是从业务对象出发,一个一个梳理。

菜品销售系统里,最基本的业务对象是:用户、菜品分类、菜品、购物车项、订单、订单明细。购物车和订单明细虽然是"中间表",但它们的字段设计决定了订单流程能不能顺畅走通。

下面是我经过反复调整后觉得比较舒服的表结构设计方案:

用户表(user)

字段类型说明
idbigint主键,自增
usernamevarchar(50)登录名,唯一
passwordvarchar(100)加密后的密码
nicknamevarchar(50)昵称,默认"用户+ID"
roletinyint0-用户 1-管理员
create_timedatetime注册时间

密码一定不能存明文,至少要用BCrypt加密,Spring Security自带的BCryptPasswordEncoder可以直接用,也可以单独加spring-security-crypto依赖,不需要引入整套Security,这样认证自己写也灵活。

菜品分类表(category)

字段类型说明
idbigint主键
namevarchar(50)分类名称,唯一
sortint排序号,越小越靠前
statustinyint1-显示 0-隐藏

菜品表(dish)

字段类型说明
idbigint主键
category_idbigint关联分类表
namevarchar(100)菜品名称
pricedecimal(10,2)价格,必须用decimal
imagevarchar(255)图片URL
descriptionvarchar(500)菜品描述
statustinyint1-上架 0-下架
salesint销量,用于排序
create_timedatetime创建时间

购物车表(cart)

字段类型说明
idbigint主键
user_idbigint关联用户
dish_idbigint关联菜品
quantityint数量
create_timedatetime加入时间

购物车表不需要存价格,因为价格随时可能变动,购物车只是"记录用户想买什么",真正结算的时候以菜品表的实时价格为准。

订单表(orders)与订单明细表(order_detail)

订单表存订单的汇总信息:用户ID、总金额、状态、收货地址(如果是点餐系统可以简化为桌号或自提)、备注、下单时间。订单明细表存每一道菜的ID、当时的单价、数量、小计。这里有个很容易忽略的点:订单明细里的价格必须是下单那一刻的快照,不能联表去查菜品表,否则菜品改价之后历史订单金额就对不上了。

订单表拆成"订单主表 + 订单明细表"两张,是为了满足一对多的数据关系,也符合电商系统的常规范式。论文里画ER图的时候这个结构也更容易讲清楚。

3.2 登录与权限:JWT方案在毕设里的正确打开方式

权限这块,毕设项目不需要引入Spring Security那套完整的过滤器链,用JWT可以讲得更清楚、代码也更直观。JWT(JSON Web Token)的核心思想是:用户登录成功后,服务端签发给用户一个Token,之后用户每次请求都带着这个Token,服务端验签通过后从里面读出用户身份,不需要在服务端保存会话状态。

代码结构一般是:

// 登录成功生成token String token = Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim("role", user.getRole()) .setExpiration(new Date(System.currentTimeMillis() + 7200_000L)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();

然后写一个拦截器(HandlerInterceptor)或者过滤器,把"需要登录才能访问"的接口保护起来。毕设项目里最直观的做法是写一个AuthInterceptor,在里面判断请求头有没有携带token,没有就返回401,有就解析并写入ThreadLocal。具体实现时注意排除登录接口、注册接口、菜品列表接口等公开接口。

我用JWT的原因还有一个:论文里可以专门写一节"基于JWT的认证授权设计",把无状态、防篡改、跨域友好这几个特点展开讲,技术含量就上来了,而且这部分是完全可以清晰展示给答辩老师看的。

3.3 下单流程的并发与事务:购物车合并、库存扣减怎么写

下单接口是整个系统里逻辑最复杂、也是答辩必问的接口。它的流程大概是:

  1. 根据当前用户ID查到购物车项。
  2. 循环校验每个菜品是否存在、是否上架。
  3. 计算总金额。
  4. 扣减库存(如果有库存字段)。
  5. 生成订单主表和订单明细表,事务性写入。
  6. 清空购物车。
  7. 返回订单ID和金额。

这个流程里最关键的是一句话:必须加事务。任何一步失败,前面写进去的数据都要回滚,否则会出现"订单创建了但购物车没清空"或者"明细少了几条"的脏数据。

@Transactional(rollbackFor = Exception.class) public Long createOrder(Long userId, String remark) { // 1. 查询购物车列表 // 2. 校验菜品状态 // 3. 计算总金额 // 4. 插入订单主表 // 5. 批量插入订单明细 // 6. 清空购物车 }

关于"重复提交"的问题,毕设阶段不需要做到分布式锁那么复杂,但至少要意识得到。后端可以通过前端按钮置灰来规避,也可以在Service层用"订单号唯一"来兜底。我把这一点写进论文里,每到答辩现场基本都会被问到,答出来就是加分项。

另外,如果你的菜品表没有库存字段,那可以不用管扣减问题,闭关销量就行。但如果有库存,就一定要考虑超卖的问题。最简单的做法是更新库存时带上条件:

UPDATE dish SET stock = stock - 1 WHERE id = ? AND stock > 0

这条SQL本身就能防止超卖,而且是数据库层面保证的,比先查再改的Java代码可靠得多。这个小细节,我说实话,很多工作两三年的开发都不一定能想到。

4. 调试阶段最容易翻车的地方:我实际踩过的坑和排查思路

4.1 金额精度:Double算钱必出问题,BigDecimal的正确用法

我第一次自己写订单模块的时候,图省事用了double存价格,算总价的时候乘来乘去,测试时出现了0.999999这种数字。前端显示金额直接显示成一串小数,差点当场社死。

金额计算在Java里锚定一个原则:所有金额类型一律用BigDecimal,数据库端用decimal(10,2)。这样从存储到计算都不会有精度问题。

还有个细节是BigDecimal的构造方式:

// 错误写法,不推荐 BigDecimal price = new BigDecimal(19.9); // 正确写法 BigDecimal price = BigDecimal.valueOf(19.9);

new BigDecimal(19.9)会把19.9的二进制浮点完整展开,得到一个很长的小数;而valueOf内部会走Double.toString(),得到的是我们肉眼看到的"19.9"。这个坑极其隐蔽,不踩一次很难有印象。

4.2 跨域请求与拦截器冲突:JWT过滤器把OPTIONS请求也拦了

前后端分离之后,前端跑在localhost:8081,后端跑在localhost:8080,跨域问题几乎必然会遇到。解决方案有这么几步:

  1. 后端写一个全局CorsFilter,放行所有跨域请求。
  2. 同时在前端Axios配置里带上withCredentials或者token头。
  3. 拦截器里要放行OPTIONS请求。

第3点是我踩过的坑。当时前端登录之后请求菜品列表,被拦截器拦住报401,我第一反应是token没带,查了半天发现是OPTIONS预检请求被我的拦截器拦截了,根本没有走到业务接口。拦截器里加一行判断就解决了:

if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; }

这个问题的排查思路值得分享一下:先看浏览器Network里到底是什么请求报错,是OPTIONS还是业务请求,再看响应头里有没有Access-Control-Allow-*。如果是OPTIONS被拦,就锁定拦截器;如果是响应头缺失,就锁定CORS配置。这种排查链路比瞎改配置有效得多。

4.3 时间格式化"水土不服":Jackson全局配置解决8小时时差

另一个让我印象深刻的坑是时间显示问题。测试的时候,我在数据库里插入一条记录,直接SQL查询看到的时间是2024-05-20 14:30:00,但是在接口返回的JSON里变成了14:30,丢掉了日期,而且加上8小时后,时间变成了晚上10点多。

原因很简单:Jackson默认把LocalDateTime序列化成数组或者ISO字符串,而SpringBoot没有配置时区会导致时间被当成UTC处理,和北京时间差了8个小时。同时MyBatis-Plus插入时默认使用了LocalDateTime.now(),本身没问题,但返回时因为配置缺失,前端拿到的字符串不合理。

我的解决方案是在统一配置里加一个Jackson全局配置:

@Bean public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { return builder -> { builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); builder.deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); builder.timeZone(TimeZone.getTimeZone("GMT+8")); }; }

这样所有接口的时间格式就统一了。从这个问题里我也学到一点:测试项目时不能只看"能不能跑",还要看返回数据格式对不对。很多话看起来是小问题,但在答辩演示时,前端页面上时间显示得漂不漂亮,直接影响整体完成度的观感。

4.4 MyBatis-Plus逻辑删除配置里藏着的坑

MyBatis-Plus有一个很常用的功能是逻辑删除,配置了@TableLogic注解后,删除操作自动变成update ... set deleted=1,查询语句也会默认带上deleted=0条件。这个功能本身很好用,但有一个坑:唯一索引和逻辑删除不兼容。

比如用户表的username设置为唯一索引,用户A的逻辑删除记录还在表里,新用户注册同一个用户名就会触发唯一索引冲突,提示"用户名已存在"。原因就是那条"已删除"的记录物理上还在表里。

解决方案有两种:一种是不用数据库唯一约束,靠Java代码先查一次来判断;另一种是把deleted字段放进唯一索引中,比如(username, deleted)。后者更稳,但需要建表的时候提前规划好。这个坑我自己当初是想破脑袋才想明白的,这里写出来给你避雷。

5. 源码之外的隐形工作:LW、调试文档与讲解演示的分寸感

5.1 毕业论文(LW)怎么和代码对得上:图表、流程图与需求追踪

标题里写得很清楚,交付物不仅有源码,还有LW(论文)、调试文档和讲解演示。很多人把写论文当成"项目做完之后抄一遍"的事,这个心态很危险。

我建议的模式是:需求分析先行,论文大纲先行,代码去填充论文的骨架。比如在论文需求分析章节定了"系统分为前台和后台,前台包含购物车、订单两大核心模块",那么代码里就必须有对应的模块。论文里画了"下单业务流程图",那么代码里就必须有这一整个流程的类和方法。答辩的时候老师常做的动作之一,就是翻开论文随便指一个流程,问你"这个在代码里怎么实现的"。

所以做这个项目时,我会刻意在代码注释上多花一点功夫。核心接口、状态流转的判断逻辑、事务边界都写清楚。写论文的时候,这些注释直接就是素材。论文里的核心代码截图也不建议大段大段地贴,而是挑一小段关键逻辑,配上解释,说明"为什么这样写、什么边界条件需要注意",反而显得更深入。

系统整体架构图也不用画得太花哨,一张包含前端、Controller、Service、DAO、数据库的分层图就够了。画图工具的选取不重要,关键是分层关系要清晰。具体的时序图(比如下单时序)可以用UML时序图展示,答辩时"可讲的故事"就多了。

5.2 调试文档的写法:不是拷贝日志,而是给答辩留一条"可复现路径"

调试文档(也叫部署说明、运行指南)是很多人忽视的部分,觉得"反正代码都能跑,写那个干嘛"。但调试文档的价值,在答辩前的环境准备阶段会体现得淋漓尽致。

好的调试文档应该包含这几块内容:

  1. 环境清单:JDK版本、Maven版本、MySQL版本,每项写明"必须在哪个版本以上,为什么"。
  2. 初始化步骤:建库SQL、导入数据的顺序、配置文件里要改哪些参数(数据库账号密码、JWT密钥、文件上传路径)。
  3. 启动顺序:先启动MySQL,再导入数据,再启动后端,最后启动前端。
  4. 验证路径:登录账号是什么、登录成功后能看到什么、下单后数据库里哪些表会发生变化。

如果调试文档做到"一个完全不懂这个项目的人照着做,十分钟之内能跑起来",那这份文档就是合格的。我在实际给同学调试代码时发现,90%的"跑不起来"问题出在环境差异上,比如JDK版本不对、MySQL密码没改、前端node_modules没装全。把这些坑提前写在文档里,后面自己调试也省心。

调试文档里还可以写一个"常见问题排查"小节,把启动时的报错截图和解决方案列出来。比如端口被占用了怎么办,数据库连接超时怎么查,前端跨域报错怎么看。这些都是最常被问到的问题,写进去之后,无论你自己复盘还是别人接手,都轻松很多。

5.3 演示安排:先讲业务流程,再讲亮点代码,最后展示异常处理

好多同学答辩演示时喜欢"看起来厉害的功能"优先展示,比如一进去就展示后台统计图表、展示管理员界面。但我个人的经验是:最好的演示顺序是复刻用户的真实使用流程。

我一般推荐的演示流程是:

  1. 从注册一个新用户、登录开始,展示用户端界面。这个阶段要展示的不是"我页面好看",而是"角色权限是生效的"。
  2. 浏览分类、查看菜品详情、搜索菜品、加入购物车。这里顺带讲一下购物车合并的逻辑,说清楚同款菜品重复添加变成了数量累加,而不是生成两条记录。
  3. 下单。把整个流程放慢,强调状态从"待接单"变为"已接单"再变为"已完成",同时打开数据库界面实时展示订单表和明细表的数据变化。这个动作非常加分,能直接证明"代码不止是页面在动,数据是真实落库的"。
  4. 切换到管理员账号,处理刚才那笔订单,展示状态更新的反向操作。
  5. 最后展示异常处理:比如下架一个菜品后,再去详情页访问,看看提示是否友好;再比如用一个无效Token访问受保护接口,看看是否返回401。这些细节比主流程更能体现工程素养。

讲到"亮点代码"的时候不要贪多,挑1-2个真正过脑子的点讲透就够了。下单事务边界、JWT拦截器、BigDecimal金额处理、库存扣减防超卖,选一个你最有把握的。选的时候有个判断标准:能讲清楚"这么做解决了什么问题、不这么做会出什么事"的,才叫亮点。光说"这行代码用了某个注解",那不叫亮点,叫念代码。

按这个顺序演示,答辩老师通常听完主流程就已经在心里打下了"这项目是完整做出来的"的底子,后面亮点和异常处理更是锦上添花。反过来说,如果一开始就展示一些"炫技"功能,结果主流程反而没讲清楚,那印象分就大打折扣了。

最后再分享一个小经验:整个项目交付物里,源码只是起点,真正让别人觉得"这人靠谱"的,是你对每个模块为什么这样设计的理解深度。建议在开发过程中随时记一些"当时纠结过什么、最后怎么定的、还有没有更好的方案"的碎片笔记,写论文和准备答辩的时候,这些碎片比任何模板都好用。这个项目和市面上那些"一键生成"的增删改查毕设最大的差别,也恰恰就在这里。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进