☰
SpringBoot+Vue+MySQL网上点餐系统毕设全流程实战
2026/10/23 5:23:23 网站建设 项目流程

又到了毕业设计季。每年这个时候,后台都会涌来一堆私信:"基础一般,想做一个能顺利过审的系统,选什么题比较稳?"我的答案一直是:网上点餐系统。理由其实不复杂——它贴近日常生活,评委不需要你费劲解释业务背景;技术栈主流又不偏门,SpringBoot + Vue + MySQL 这套组合既能体现完整的全栈能力,又把难度控制在一个人能驾驭的范围内。下面我会把整个项目从需求拆解、表结构设计、核心接口实现,到论文文档组织、部署踩坑的完整思路,掰开揉碎讲清楚,给准备选这个题或者已经开工的同学一份可以直接照做的实操参考。

真不是我偷懒才推荐这个题目。你去看每年各大高校的毕设选题列表,"点餐""商城""图书管理"永远是最常见的三类。点餐系统能常年霸榜,是因为它天然符合毕业设计的所有评审期待:有清晰的用户角色、有完整的业务流程、有真实的应用场景,而且一个人在一个学期内真的能做完。更重要的是,面试时你把它讲清楚,"你解决了什么问题""你碰到了什么困难"这两件事都有现成的素材,不会卡壳。这篇文章我就围绕"源码 + 数据库 + 论文 + 部署文档"这条完整交付链,把我做完这套系统后沉淀下来的真实经验全部写出来。

1. 毕业设计选"网上点餐系统",这个题目的真实分量

1.1 为什么它一直是我的推荐榜常客

每逢选题季,很多人的第一反应是选"看起来高级"的题目,比如智能推荐、大数据分析、人脸识别。结果做两个月发现环境都搭不明白,最后草草糊弄,答辩被问得哑口无言。点餐系统恰恰相反,它看起来"普通",但内含的完整度非常高。

先说业务模型。一套点餐系统天然分成用户端和管理端:用户端是浏览菜品、加购物车、下单、查看订单;管理端是菜品维护、分类管理、订单处理。这个划分非常工整,画用例图的时候一清二楚,写需求分析章节的时候也不需要硬编造什么"痛点"。

再说技术覆盖面。SpringBoot负责后端接口与业务逻辑,Vue负责前端交互与数据渲染,MySQL负责持久化存储,前端还要处理跨域、图片上传、路由守卫,后端要处理JWT鉴权、事务、分页。一个系统拉下来,前后端分离的基本功全部过了一遍。这些技能不是背概念,是真正要写代码实现的,简历上写"独立完成前后端分离项目"就非常有底气。

最后是演示效果。点餐系统有天然的操作流程:打开小程序风格的界面、点几个菜、提交订单、在管理后台看到新订单、修改状态。整套流程可视化极强,演示三分钟,评委基本就能理解你做了什么。相比"一个后台管理系统只改了个名字"的常规项目,这种有真实操作闭环的项目,印象分会高不少。

1.2 一套完整交付物不止是"代码能跑"

很多同学以为毕业设计就是"把代码写完,能运行就完事了",等到提交材料的时候才发现缺这个缺那个。我做完这套系统才真正意识到,所谓"完整的毕业设计"是一个交付包,里面四样东西缺一不可。

  • 项目源码:后端 SpringBoot 工程 + 前端 Vue 工程。注意源码不是堆在网盘里就行,是要保证"新的电脑拉下来,按文档操作能跑起来"。
  • 数据库脚本:建库、建表、插入初始数据的 SQL 脚本。这是老师打开你数据库设计文档后逐张核对表结构的关键依据。
  • 毕业论文:按照学校提供的模板写,一般包含选题背景、可行性分析、需求分析、总体设计、详细设计、系统实现、系统测试。
  • 部署文档:从环境安装到启动成功的每一步操作说明。这份文档在答辩时容易被忽略,但指导老师经常会照着它亲自跑一遍。

这四样东西的优先级怎么排?源码是地基,数据库是骨架,论文是门面,部署文档是说明书。我见过源码写得不错但数据库建表乱七八糟的项目,答辩时被老师抓着E-R图问了三分钟,最后只能承认"表是随便建的"——那真的太亏了。所以后面我会专门用一章讲数据库设计和论文组织,这两块的"性价比"远比你想象中高。

1.3 需求拆解:先把用户端和管理端画清楚

拿到题目的第一步不是写代码,而是写需求。我当时花了两天时间,把需求拆成了两张功能清单。

用户端的功能我控制在七个:注册登录、菜品分类浏览、菜品关键词搜索、购物车管理、提交订单、订单状态查看、个人中心(修改头像和手机号)。管理端的功能我控制在四个:管理员登录、分类管理、菜品管理、订单管理。其中订单管理要支持按订单状态筛选,菜品管理要支持上下架切换。除此之外我一概不做——不做真实的支付网关对接,只做"模拟支付";不做多商户入驻,只做一个商家后台;不做推荐算法,只做简单分页。

这里有个很关键的心得:需求分析的加分项不在于你做了多少功能,而在于你敢不敢写"不做什么"。论文里的需求分析章节,如果能明确写出"本系统不涉及真实第三方支付、不涉及多商户平台运营,原因是……",老师会觉得你是经过思考的,而不是想到哪写到哪。我自己在论文里就专门写了一小节"系统边界与约束",答辩时甚至被夸过这里的表述清晰。

2. SpringBoot + Vue + MySQL 的选型逻辑,不只是"大家都在用"

2.1 SpringBoot 到底帮你省了什么

如果你搜过 SSM 框架的教程,再回来用 SpringBoot,感受会非常强烈。SSM 时代,你得手动配置 Spring 的 Bean 扫描、SpringMVC 的视图解析器、MyBatis 的 SqlSessionFactory,一个 web.xml 要写几十行。而 SpringBoot 的核心思想就一句话:约定大于配置。它通过 starter 把常用组件的自动配置全部封装好,你只需要引入依赖,写几个配置项,项目就能跑。

我用的组合是 SpringBoot 2.7.x + JDK 1.8 + MyBatis-Plus。选 2.7 而不是 3.x,是因为 3.x 对 JDK 版本要求更高,而且很多网上教程还是 2.x 的写法,遇到问题更容易搜到答案。MyBatis-Plus 是我特意加的,理由很现实:毕设的单表 CRUD 占了一大半,用 MyBatis-Plus 的 BaseMapper,连 SQL 都不用写,极大节省时间。比如菜品表直接继承BaseMapper<Dish>,selectById、selectList、insert这些方法就全都有了。

这里给一段我当时后端的核心依赖,大体的样子是:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.19.2</version> </dependency>

2.2 前端为什么用 Vue + Element UI

如果是五年前,做毕业设计的前端主流方案还是 JSP + Bootstrap。但 JSP 是服务端渲染,前端页面和后端 Java 代码强耦合,改一个按钮样式都要重新编译部署,开发体验极其痛苦。Vue 最大的优势是组件化开发和前后端分离:页面是静态资源,通过 axios 调用后端接口拿到 JSON 数据再渲染。你可以在本地把前端页面调试得漂漂亮亮,后端接口只要保证数据格式正确就行。

Vue 的上手曲线也比 React 平缓很多。我用的是 Vue 2.6 + Element UI,组件丰富,表格、表单、弹窗、分页这些毕业设计里最常见的界面组件都有现成的。配合 Vue Router 做页面跳转、Vuex(或者 localStorage)做登录状态持久化,一套下来非常顺手。等你有基础了再升级 Vue 3 + Element Plus 完全无障碍。

有一个细节值得提一下:前端项目我用了 npm 作为包管理器,但国内网络直接 npm install 经常会卡在 node-sass 这种编译型依赖上。我的处理方式是先把镜像源切到国内镜像,再安装依赖。这个点我也会写进部署文档,后面会再提到。

2.3 MySQL 的定位和版本选择

数据库层面,点餐系统这种强事务业务,毫无疑问选 MySQL 关系型数据库。点餐涉及订单金额计算、库存扣减(或者说菜品数量校验)、购物车数据一致性,这些场景需要事务保证,不能用 MongoDB 这类非关系库硬扛。MySQL 我用的是 5.7,稳妥、教程多、服务器上也好装;现在新机器装 8.0 也完全没问题,只是要注意驱动版本要用mysql-connector-j8.x,并且连接串里要加serverTimezone=Asia/Shanghai,不然时间字段会出现 8 小时的时区偏差。

关于缓存,很多人会问要不要加 Redis。我的建议是:毕设可以把 Redis 作为扩展点写进论文,但不要真的依赖它。点餐系统的核心链路是下单,只要接口写得合理、表设计规范,MySQL 完全扛得住答辩演示的压力。强行加 Redis 会引入缓存一致性、缓存穿透这些新问题,反而给自己挖坑。论文里写一句"本系统预留了 Redis 缓存优化方案,可作为后续扩展",老师看了只会觉得你视野宽。

3. 数据库设计的五个关键决策

数据库设计是整个项目里最值得投入时间的地方。答辩时老师可以不听你讲代码,但一定会看你的 E-R 图和数据库表结构。我把我最终的建表思路拆成五个决策点,逐个说明。

3.1 用户表不分表,一个 role 字段解决权限问题

我第一次设计的时候,脑子里总觉得"用户"和"管理员"应该是两张表,一个是普通用户,一个是负责后台管理的账号。后来写代码才发现这个想法给自己增加了一堆不必要的麻烦:登录接口要判断走哪张表,JWT 里要区分用户类型,每次权限校验都要多查一次。实际上绝大多数毕设系统里的用户和管理员,差别就一个字段:角色。

所以我最终的用户表长这样:

CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(加密后)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `avatar` varchar(200) DEFAULT NULL COMMENT '头像地址', `role` tinyint NOT NULL DEFAULT '1' COMMENT '角色:1-普通用户,2-管理员', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

密码一定不要明文存储,我用了 BCrypt 加密。这条在答辩时几乎是必考题——"用户密码怎么保存的?"你要是回答"明文",那基本就凉了一半。BCrypt 实现也不复杂,Spring Security Crypto 工具里直接有BCryptPasswordEncoder,不用引入整个 Spring Security 就能用。

3.2 菜品与分类:应用层维护关联,不建物理外键

菜品dish和分类category是一对多关系,这个大家都能想到。但我的建议是:不要真的在菜品表上建外键约束。原因不是"外键性能差"这种抽象理由,而是毕设阶段你会频繁地在后台修改、删除分类,有物理外键的情况下,你删掉一个分类,数据库会强制拒绝删除或者级联删除一堆菜品,非常容易让前端页面显示异常。保持逻辑关联,靠应用层判断分类是否存在,反而更灵活。

菜品表的核心字段,我在建表时专门注意了status字段:

CREATE TABLE `dish` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '菜品名称', `category_id` bigint NOT NULL COMMENT '分类ID', `price` decimal(10,2) NOT NULL COMMENT '价格', `image` varchar(200) DEFAULT NULL COMMENT '图片地址', `description` varchar(500) DEFAULT NULL COMMENT '描述', `status` tinyint NOT NULL DEFAULT '1' COMMENT '1-上架,0-下架', `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_category_id` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

status字段我单独强调一下。很多同学会把"删除菜品"做成物理删除——直接从表里 DELETE 一行。这很危险,因为历史订单里可能引用这个菜品,你还没做订单明细快照的话,就会出现"订单详情打不开"的 bug。我的做法是做逻辑删除(或者干脆用上下架状态),用户端看不到下架菜品,但数据库里数据还在。MyBatis-Plus 有现成的逻辑删除配置,@TableLogic注解标一下即可,非常方便。

3.3 订单表与订单明细表:拆开存,理由藏在"历史快照"里

订单是点餐系统的核心,表设计要把"一次下单"拆成订单主表和订单明细表两张表。订单主表orders记录的是这次订单的整体信息:订单号、用户、总金额、状态、收货信息;订单明细表order_detail记录的是这个订单包含哪些菜品、每道菜多少钱、数量是多少。

CREATE TABLE `orders` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号', `user_id` bigint NOT NULL, `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0-待支付 1-已支付 2-制作中 3-配送中 4-已完成 5-已取消', `receiver_name` varchar(50) DEFAULT NULL COMMENT '收货人姓名', `receiver_phone` varchar(20) DEFAULT NULL COMMENT '收货人电话', `receiver_address` varchar(200) DEFAULT NULL COMMENT '收货地址', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_id` (`user_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `order_detail` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_id` bigint NOT NULL, `dish_id` bigint NOT NULL, `dish_name` varchar(100) NOT NULL COMMENT '菜品名称快照', `dish_image` varchar(200) DEFAULT NULL COMMENT '菜品图片快照', `price` decimal(10,2) NOT NULL COMMENT '购买时单价快照', `quantity` int NOT NULL COMMENT '购买数量', PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

为什么订单明细里要存dish_name和price这两个"冗余"字段?这是我当时想明白就特别兴奋的一个点。因为菜品价格和名称是会变的。今天一份宫保鸡丁 28 元,下周老板调价到 32 元,如果订单明细不存快照,你查三个月前的订单,看到的会是"32 元"而不是当时的"28 元",这在真实的商业系统里是不可接受的。订单明细表必须保存下单那一刻的菜品名称和单价快照——这叫历史数据的可追溯性。答辩时把这个点讲出来,是一个非常亮眼的加分项。

3.4 购物车表:我选择了落库而不是前端 LocalStorage

购物车有两种做法:一种存前端 localStorage,简单、不用写接口;另一种是建cart表,后端存储。我两条路都试过,最终选了建表落库。核心原因有两点。

第一是数据一致性。localStorage 存购物车,用户换个浏览器、清个缓存,购物车就没了。而且前端存的数据在后端完全不可控,用户下单时后端没法校验"这些东西是不是真的在购物车"。如果购物车在后端,用户点"下单"按钮时,后端只需要根据用户 ID 从数据库查出购物车内容,生成订单,逻辑非常干净。

第二是表设计完整度。论文里的 E-R 图如果少了购物车实体,数据库设计章节会显得单薄;有了购物车表,整个系统的实体关系就更完整,答辩时老师也更容易理解你的订单生成链路。

购物车表我用了唯一索引处理一个经典问题:同一个用户同一个菜品,要么不存在,要么只有一行记录,数量累加。

CREATE TABLE `cart` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL, `dish_id` bigint NOT NULL, `quantity` int NOT NULL DEFAULT '1', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_dish` (`user_id`, `dish_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

配合 MySQL 的INSERT ... ON DUPLICATE KEY UPDATE,可以实现"用户再加同一道菜时,直接更新数量而不是插入新行"。这个写法在接口实现部分会再展开。

3.5 索引、时间字段和逻辑删除:容易被老师追问的细节

表结构交上去之后,老师有概率抽查几个"你没想过"的地方。我在这里总结了三个细节,都是我自己踩过或见过别人踩的。

第一是索引。订单表按用户查订单是最频繁的操作,我在user_id上建了普通索引;订单状态筛选也常用,我在status上建了索引。购物车表user_id和dish_id建了联合唯一索引,既防重复又加速查询。索引不是越多越好,但这个量级的系统,这几条索引足够。

第二是时间字段。所有表我都加了create_time,订单表额外加了update_time。如果用的是 MyBatis-Plus,可以在字段上配置自动填充,插入时自动写入当前时间,更新时自动刷新。连接串里必须写serverTimezone=Asia/Shanghai,否则你会在某天突然发现数据库时间和本地时间差了 8 个小时,排查半天。

第三是逻辑删除。上文提过,菜品表我建议用status字段实现上下架,而不是 DELETE。如果用了 MyBatis-Plus 的逻辑删除功能,要注意它会自动往所有查询里拼接WHERE deleted = 0。我当时在部署后出现过莫名查询不到数据的情况,最后发现就是逻辑删除字段配置得有问题,全局字段名和实体字段对不上。这条坑我放到部署问答里细说。

4. 核心接口的设计:从点菜到下单一气呵成

数据库设计好了,接下来就是最核心的接口开发阶段。这一章我按业务链路的顺序写:先讲统一的接口返回规范,然后是购物车,然后是下单事务,最后是订单状态流转。这些接口直接决定系统好不好用,也是论文里"详细设计"章节的主要素材。

4.1 统一返回结构和全局异常处理

前后端分离项目最忌讳的就是每个接口返回格式都不一样:这个接口返回{code: 1, data: {...}},那个接口返回{success: true, result: [...]}。前端联调时只能在 axios 拦截器里为每个接口单独写判断逻辑,代码写到你怀疑人生。

我的做法是定义一个泛型返回类,所有接口统一走它。

@Data public class Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMsg("success"); r.setData(data); return r; } public static <T> Result<T> error(String msg) { Result<T> r = new Result<>(); r.setCode(500); r.setMsg(msg); return r; } }

再配一个全局异常处理器,用@RestControllerAdvice接收所有业务异常,转换成统一的 Result 结构返回。这样前端 axios 响应拦截器只需要判断res.data.code === 200,其他一律弹出错误提示。这个设计在论文里叫"统一响应体设计",写系统设计章节时是现成的有力素材。

4.2 购物车接口:数量变更用唯一索引加更新

购物车的核心接口就四个:查询购物车列表、加入购物车、修改数量、删除购物车项。加购物车的实现可以体现出你对 SQL 的掌握程度。我的做法是:

public void addCart(Long userId, Long dishId, Integer quantity) { Dish dish = dishMapper.selectById(dishId); if (dish == null || dish.getStatus() == 0) { throw new BusinessException("菜品不存在或已下架"); } cartMapper.addOrUpdate(userId, dishId, quantity); }

对应的 SQL 利用ON DUPLICATE KEY UPDATE:

INSERT INTO cart(user_id, dish_id, quantity, create_time) VALUES(#{userId}, #{dishId}, #{quantity}, NOW()) ON DUPLICATE KEY UPDATE quantity = quantity + #{quantity}

这一步的精妙之处在于:如果购物车里本来就没有这道菜,就插入一行新的;如果已经有了,就把数量累加。不需要先查询再判断再更新,一条 SQL 完成,也不会出现并发时插入两条重复记录的问题。

查询购物车列表时,需要联表查出菜品名称、价格、图片等信息。我用的是 MyBatis-Plus 的selectMaps加自定义 SQL 联表查询,接口返回给前端的数据结构为:

{ "id": 1, "dishId": 2, "dishName": "鱼香肉丝", "image": "http://localhost:8080/images/xxx.jpg", "price": 25.00, "quantity": 2 }

4.3 下单接口:事务边界和订单号幂等

下单是整个系统中最核心的接口,没有之一。我第一次写的时候以为就是把购物车数据复制到订单表,后来才意识到这里面藏着三个关键问题:事务、幂等、状态校验。

先看最终的核心代码结构:

@Transactional(rollbackFor = Exception.class) public OrderVO createOrder(Long userId, OrderRequest request) { // 1. 查询购物车,空则抛异常 List<Cart> cartList = cartMapper.selectByUserId(userId); if (CollectionUtils.isEmpty(cartList)) { throw new BusinessException("购物车为空,无法下单"); } // 2. 校验菜品状态并计算总金额 BigDecimal total = BigDecimal.ZERO; for (Cart c : cartList) { Dish dish = dishMapper.selectById(c.getDishId()); if (dish == null || dish.getStatus() == 0) { throw new BusinessException("菜品已下架:" + dish.getName()); } total = total.add(dish.getPrice().multiply(BigDecimal.valueOf(c.getQuantity()))); } // 3. 生成订单 Orders order = new Orders(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(0); order.setReceiverName(request.getReceiverName()); order.setReceiverPhone(request.getReceiverPhone()); order.setReceiverAddress(request.getReceiverAddress()); orderMapper.insert(order); // 4. 生成订单明细,保存菜品快照 for (Cart c : cartList) { Dish dish = dishMapper.selectById(c.getDishId()); OrderDetail detail = new OrderDetail(); detail.setOrderId(order.getId()); detail.setDishId(dish.getId()); detail.setDishName(dish.getName()); detail.setDishImage(dish.getImage()); detail.setPrice(dish.getPrice()); detail.setQuantity(c.getQuantity()); orderDetailMapper.insert(detail); } // 5. 清空购物车 cartMapper.deleteByUserId(userId); return orderVO; }

@Transactional注解保证了从查购物车到写订单明细再到清空购物车,整个过程要么全部成功,要么全部回滚。比如第四步某条明细插入失败,整个订单也不会留下半截数据。这个知识点对应论文里的"事务一致性设计",答辩时基本必讲。

订单号幂等是我重点提的一个坑。我一开始用System.currentTimeMillis()拼随机数生成订单号,测试时发现并发下单会出现重复订单号,撞上了订单表的唯一索引uk_order_no,程序直接报错。后来改成"时间戳 + 三位随机数 + 用户ID后两位",重复概率就低了很多。但最稳妥的做法其实是让前端在点下单按钮时生成一个客户端请求唯一编号,后端记录后做幂等判断。毕设场景下,订单号用唯一索引兜底 + 前端防止重复点击,这个程度就已经合格了。

4.4 订单状态机:谁有权利改变状态

订单状态我定义了 6 个值:0 待支付、1 已支付/待制作、2 制作中、3 配送中、4 已完成、5 已取消。这里有一个很容易忽略的设计问题:状态不是随便哪个接口都能改的。

用户端能触发的只有两个:待支付时取消订单、支付模拟按钮(把待支付改成已支付)。管理端能触发的有:从已支付改成制作中、制作中改成配送中、配送中改成已完成。后端在每个接口里都要校验当前订单状态是否合法,比如管理端不能跳过"制作中"直接把待支付订单改成"配送中"。

我在代码里写了一个简单的状态校验工具方法:

public void checkStatus(int currentStatus, int expectStatus) { if (currentStatus != expectStatus) { throw new BusinessException("当前订单状态不允许此操作"); } }

维护这份状态流转表的另一个好处是写论文系统详细设计时,可以直接画一张"订单状态流转图"放进文档里。这是论文里最直观体现你设计能力的地方之一。

4.5 文件上传与跨域:两个让前端黑屏的元凶

做完核心链路后,我还为菜品上传图片、以及前后端联调时的跨域问题耗了不少时间,这里也把经验写出来。

图片上传我的方案是后端接收 MultipartFile,保存到本地磁盘目录,再返回一个相对路径。需要特别注意的是静态资源映射。SpringBoot 默认不会把磁盘上的某个目录当作静态资源暴露出去,你不配置的话,前端拿到图片地址/images/xxx.jpg请求的时候会直接 404。必须在配置类里加上:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/images/**") .addResourceHandler("file:" + uploadPath); } }

跨域问题则是前后端分离项目的第一道坎。前端跑在localhost:8080,后端跑在localhost:8090,协议、地址、端口任何一个不同就是跨域。我当时的处理是后端直接允许跨域:

@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); config.setAllowCredentials(true); return new CorsFilter(new UrlBasedCorsConfigurationSource(config)); } }

这样前端不用配代理,联调时最省心。生产部署时如果要放进 Nginx,再把前端的/api路径反向代理到后端,同时去掉后端的跨域配置,那一套属于进阶玩法,毕设演示阶段不必折腾。

5. 论文、部署文档和答辩准备:让老师挑不出硬伤

编码阶段结束,真正的"交付物收尾阶段"才开始。我见过程序写得不错但论文一塌糊涂的项目,也见过部署文档写得不够细,指导老师自己都跑不起来的情况。这一章我专门讲怎么把论文、部署文档和答辩准备这三件事做扎实。

5.1 论文的骨骼:从选题背景到测试结果

论文的结构多数学校有固定模板,但逻辑骨架基本万变不离其宗:选题背景与意义、需求分析、系统总体设计、系统详细设计与实现、系统测试、总结与展望。

最容易出彩的是需求分析章节。不要只贴两张用例图就完事,我当时的做法是给每个核心功能写一段"用例描述",包含参与者、前置条件、主事件流、异常事件流。比如"提交订单"用例,主事件流是:用户进入购物车、点击提交、填写收货信息、系统创建订单、清空购物车;异常事件流是:购物车为空提示、菜品已下架提示。这部分内容一旦写出来,答辩时你描述需求的能力会明显比别的同学扎实。

系统详细设计章节,核心不是贴大段代码,而是画图和解释关键机制。我放了三样东西:E-R 图、实体类关系图、订单状态流转图。然后挑几个核心接口(下单、订单状态修改)贴少量代码,配合文字解释"为什么要用事务""为什么订单明细要存快照"。记住一个原则:论文里的代码是用来辅助说明设计的,不是用来凑字数的,大段源码贴进去反而显得没有提炼能力。

系统测试章节要真做数据说话。功能测试列一个测试用例表:用例编号、测试步骤、预期结果、实际结果、是否通过。性能测试我用 JMeter 对登录接口和菜品列表接口做了一次基本的并发请求测试,比如 50 个线程循环 10 次,记录平均响应时间。不需要复杂的指标分析,一张响应时间表就说明你的接口是经得起基本考验的。这部分很多同学省略,但恰恰是答辩时体现工作量的地方。

5.2 部署文档的写法:照着做能跑起来才算合格

部署文档的要求只有一条:让一个从来没跑过这个项目的人,不问你任何问题,照着文档从零走到系统启动成功。我当时写完部署文档后做了一件事:把文档发给一个不懂这个项目的室友,让他照着操作,全程我只看不说话,把每一步他卡住的地方记录下来,回去改文档。这个方法虽然简陋,但效果出奇地好。

一份合格的部署文档大概要包含这几部分:

  • 环境准备:JDK 1.8、Maven 3.6+、Node 16+、MySQL 5.7+,以及每个环境的安装验证命令。
  • 数据库初始化:打开 MySQL,执行create database order_system default character set utf8mb4;,然后导入项目中的sql/init.sql脚本。
  • 后端启动:修改application.yml里的数据库账号密码,执行mvn spring-boot:run或打包成 jar 后java -jar启动。
  • 前端启动:进入前端目录,npm install安装依赖,npm run dev启动开发环境。
  • 访问地址:前端http://localhost:8080,后端接口http://localhost:8090,内置管理员账号和测试用户账号。
  • 常见问题:按我下面要讲的报错排查顺序写成问答形式放进去。

后端的一个核心配置示例:

server: port: 8090 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/order_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 5MB max-request-size: 10MB mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

5.3 部署阶段的高频报错和排查顺序

部署阶段最常见的坑,我把它们汇总成了一个排查清单,写进部署文档里。这里也分享给各位:

第一个是数据库连接失败。报错信息通常是Communications link failure或者Access denied for user。排查顺序是:MySQL 服务是否启动、账号密码是否正确、application.yml里的数据库名是否和实际创建的库一致、端口是否是 3306。注意 MySQL 8.0 的密码认证插件和 5.7 不同,如果驱动版本不一致会报Public Key Retrieval is not allowed,这时需要在连接串里加allowPublicKeyRetrieval=true。

第二个是端口占用。后端默认端口如果设成 8080,而前端开发服务器也是 8080,启动时就会报Port already in use。我的处理办法是后端用 8090,前端用 8080,从一开始就不冲突。

第三个是前端依赖安装失败。npm install报错大概率是网络问题,把镜像切到国内镜像再试。如果依然报错,删除node_modules和package-lock.json后重装。这条我见到的频率非常高,部署文档里一定要写。

第四个是 MyBatis-Plus 逻辑删除导致查询查不到数据。我在前面提过,@TableLogic注解的字段名是deleted,但我在建表时根本没建这个字段,结果 MyBatis-Plus 自动拼接WHERE deleted = 0,所有查询全部返回空列表。解决方法是把deleted字段真正加到表里,或者把逻辑删除全局配置关掉,用status字段自己控制。这个坑排查了我整整一个晚上,务必引以为戒。

5.4 答辩前我用一张表来自查

答辩前一周,我列了一张自查表,每天按着表逐项过一遍。现在回头看,这份自查表帮了大忙。

检查项具体内容是否通过
功能链路用普通用户走完:注册→登录→浏览→加购→下单→支付→查看订单是
管理链路用管理员走完:登录→新增分类→新增菜品→处理订单状态是
异常场景空购物车下单、菜品下架后加购、重复下单是
数据复查删除菜品后,历史订单明细仍能正常显示是
论文核对数据库表名和字段与论文 E-R 图完全一致是
部署演练关闭全部环境,按部署文档从零启动是

这里重点说一下最后两项。很多同学论文里画 E-R 图的时候随手一画,字段名跟数据库里实际的都不一样,答辩时老师只要一细看就会发现前后矛盾,印象分掉得很快。我的方法是把数据库里所有表的字段导出来,和论文的数据库设计章节逐个比对,确保一字不差。部署演练也值得做,因为演示当天电脑状态是不可控的,你预先完整启动过一遍,哪怕现场出问题也能快速定位。

答辩问答题我也做了准备。老师会关心的问题其实高度可预测:为什么选这个课题、系统有哪些角色、订单状态怎么流转、数据库表是怎么设计的、遇到的比较难的问题是什么、你怎么解决的。这些问题我前面都覆盖到了。只要你在做项目时是真正理解每一个设计决策的,答辩现场表现就不会差。

最后分享一点我自己的体会:毕业设计本质上是一次"把软件工程流程走一遍"的训练。源码、数据库、论文、部署文档这四样东西,每一件都有它的用途,不要觉得文档是在应付差事。我见过不少同学把大量时间花在调整页面样式上,结果数据库设计只有一张潦草的图,答辩时被老师问两句就卡壳。反过来,只要你把需求边界讲清楚、表结构说明白、核心链路能跑通,这已经是一个能过审、甚至能拿去面试聊一聊的作品。如果做完还有余力,给系统补上单元测试和一份简单的 JMeter 压测报告,这个加分项真的划算。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询