ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring Boot校园外卖平台毕设全攻略:从数据库设计到部署上线

Spring Boot校园外卖平台毕设全攻略:从数据库设计到部署上线 每年到了毕业设计选题的时候总有不少人纠结做什么题目。如果你拿到的题目是“Spring Boot校园外卖平台”这其实是个挺聪明的选择——它既是学校周边真实需求的缩影又刚好把Web开发的主干技术都串起来了用户登录、商品展示、购物车、下单、订单状态流转、后台管理一整套电商闭环。我带过不少学生做完这个题目可以负责任地说它难度不算高但踩坑的地方特别多。这篇文章就把整个项目从技术选型、数据库设计、后端核心代码到部署上线的完整思路捋一遍适合正在做毕设、想快速复制并跑通的人参考。1. 做校园外卖毕设先把这些东西想明白1.1 选择Spring Boot而不是别的很多人在选题时纠结过SSM、Spring Boot还是Spring Cloud。SSMSpring Spring MVC MyBatis是很多学校教学大纲里的老熟人但是配置麻烦光是XML文件就能劝退初学者。Spring Boot最大的价值在于“约定大于配置”它把大量繁琐的配置自动化了让你把精力集中在业务逻辑上。而且现在企业里用Spring Boot做微服务底座是非常主流的选择写到简历上比SSM好看很多。Spring Cloud虽然高大上但对于毕设来说基本属于杀鸡用牛刀。校园外卖这个场景单体应用完全能撑住强行拆微服务只会增加服务注册、配置中心、网关这一堆额外复杂度自己写起来痛苦答辩时也容易被老师追问到回答不上来。所以我的结论很明确单体Spring Boot项目配合MySQL、Redis、MyBatis-Plus这个组合是性价比最高的。1.2 系统角色与核心业务梳理动手写代码之前一定要先把角色和业务理清楚。校园外卖平台至少有四类角色用户端学生、商家端校内餐厅或个体商家、配送端骑手或兼职学生和管理员。不同的角色对应不同的功能边界。用户端注册登录、浏览门店、搜索菜品、添加购物车、下单、支付、查看订单状态、取消订单、售后申请。商家端门店信息维护、菜品上架下架、接单、出餐、查看订单列表。骑手端接单配送、更新配送状态。管理员端审核商家入驻、运营数据统计、系统公告管理。这里面最核心的主线就是“下单-接单-配送-完成”这条状态链路其他所有功能都是围绕这条链路展开的。设计阶段先把这条链路画清楚代码写起来会顺畅很多。1.3 技术选型清单直接给出我推荐的最终技术清单你可以照着准备环境层面技术/工具说明后端框架Spring Boot 2.7.x稳定教程多避坑容易ORMMyBatis-Plus单表CRUD零SQL节省大量时间数据库MySQL 8.x主流功能完善缓存Redis存购物车、会话和热点菜品权限认证Sa-Token比Spring Security更容易上手定时任务Spring Scheduled处理超时未支付订单前端管理端Vue 3 Element Plus前后端分离后台界面标准组合前端用户端H5 / 微信小程序二选一小程序演示效果更好接口调试Apifox/Postman联调必备这个清单考虑了“能用”和“能讲清楚”两个维度。选的都是一线城市企业里常用的技术面试时也能聊几句而不是纯粹的玩具项目。2. 数据库设计订单、用户、商品表怎么建才优雅2.1 核心数据表数据库是整个外卖系统的地基。很多学生上来就用工具自动生成一堆表结果字段不完善后面改来改去非常痛苦。我建议至少建这几张表-- 用户表 CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT, openid VARCHAR(64) DEFAULT NULL COMMENT 微信openid, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, nickname VARCHAR(50) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL, role TINYINT NOT NULL DEFAULT 1 COMMENT 1-用户 2-商家 3-配送员 0-管理员, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, deleted TINYINT DEFAULT 0 COMMENT 逻辑删除, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;-- 门店表 CREATE TABLE shop ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 店主用户id, name VARCHAR(100) NOT NULL, logo VARCHAR(255), notice VARCHAR(255) COMMENT 公告, status TINYINT DEFAULT 1 COMMENT 1-营业 0-打烊, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT门店表;-- 菜品表 CREATE TABLE dish ( id BIGINT NOT NULL AUTO_INCREMENT, shop_id BIGINT NOT NULL, name VARCHAR(100) NOT NULL, image VARCHAR(255), price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0 COMMENT 库存, status TINYINT DEFAULT 1 COMMENT 1-上架 0-下架, PRIMARY KEY (id), KEY idx_shop (shop_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT菜品表;购物车表其实有两种设计思路。一种是把购物车数据落到MySQL里简单直接另一种是存到Redis里用Hash结构key为cart:用户IDfield为菜品IDvalue为数量。我倾向于Redis方案因为购物车是高频读写场景走Redis可以减少数据库压力而且在答辩时能讲出“我用Redis优化了购物车读写性能”这种亮点。2.2 订单表与状态机订单表是整个系统最核心的表设计得不好后面要返工。CREATE TABLE orders ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单编号, user_id BIGINT NOT NULL, shop_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待支付 1-待接单 2-配送中 3-已完成 4-已取消, address VARCHAR(255) NOT NULL COMMENT 配送地址, consignee VARCHAR(50) NOT NULL COMMENT 收货人, phone VARCHAR(20) NOT NULL, remark VARCHAR(255) DEFAULT NULL, pay_time DATETIME DEFAULT NULL, delivery_time DATETIME DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;订单状态字段建议用TinyInt数字表示不要直接存字符串。因为数字占空间小、查询快而且代码里可以做成枚举见名知意写起来也不会打错单词。状态流转是答辩常问的点。订单的状态不是随便跳的必须有一套状态机来控制。比如“待支付”只能跳到“已支付”或“已取消”“已支付”之后才能“待接单”“配送中”才能“已完成”。如果用户在后端接口里手动传一个状态让你直接改那系统就崩了。我用一个枚举加Map来实现约束public enum OrderStatus { UNPAID(0, 待支付), PAID(1, 待接单), DELIVERING(2, 配送中), FINISHED(3, 已完成), CANCELLED(4, 已取消); private final int code; private final String desc; private static final MapInteger, ListInteger TRANSITIONS new HashMap(); static { TRANSITIONS.put(0, Arrays.asList(1, 4)); // 待支付 - 已支付 / 取消 TRANSITIONS.put(1, Arrays.asList(2, 4)); // 待接单 - 配送中 / 取消 TRANSITIONS.put(2, Arrays.asList(3)); // 配送中 - 已完成 } }在做状态更新时先校验当前状态是否允许流转到目标状态不允许就抛异常。这样能挡住很多非法操作也向老师展示了你对业务边界的把控能力。2.3 数据库索引与性能考虑在设计表结构时顺手把索引也规划好。外卖系统最主要的查询是根据用户ID查订单列表、根据门店ID查菜品、根据订单状态查待处理列表。所以orders表的user_id、dish表的shop_id、订单表的status字段都应该建索引。另一个容易忽略的点是金额字段一定要用DECIMAL而不是FLOAT或DOUBLE。浮点数在计算金额时会出现精度丢失比如0.1 0.2 0.30000000000000004这在支付场景是不可接受的。DECIMAL的精度足够而且在此基础上的计算也是确定的。3. Spring Boot后端配置与核心代码3.1 项目初始化与pom依赖创建项目我推荐两种方式用Spring Initializr官网生成或者直接用IDEA内置的Spring Initializr。两者的核心依赖是一样的主要就是下面这些parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdcn.dev33/groupId artifactIdsa-token-spring-boot-starter/artifactId version1.37.0/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies注意我用了MyBatis-Plus而不是纯MyBatis。理由很简单MyBatis-Plus内置了单表的增删改查方法你不需要为每一张表写一堆重复的Mapper XML这样能把时间省下来对于核心业务逻辑的打磨上。对于多表关联查询比如“查询订单详情时带出菜品信息”自己写SQL也是完全可控的。3.2 application.yml配置细节配置文件的坑也不少我直接给一份可以跑的配置server: port: 8080 spring: application: name: campus-takeout datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/campus_takeout?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: yourpassword redis: host: localhost port: 6379 database: 0 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true 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这里有几个特别容易踩坑的地方serverTimezone参数不设置会报时区错误必须加上Asia/ShanghaiuseSSLfalse本地连接MySQL不加这个参数可能会警告甚至报错map-underscore-to-camel-case开启后数据库的created_at字段才能自动映射到Java的createdAt属性logic-delete-field配置了逻辑删除以后MyBatis-Plus会在查询时自动追加deleted 0条件删除时自动变成UPDATE set deleted 1这个机制非常实用但要注意在业务代码里不要再手动写delete from语句3.3 用户登录与JWT鉴权校园外卖一般有两种登录方式微信授权登录和手机号验证码登录。毕设阶段没有企业资质接微信开放平台很麻烦所以手机号验证码或者账号密码是最常见的方案。我建议做成微信授权登录的模拟版本用户在注册时绑定一个模拟openid再配一个token用于后续身份认证。使用Sa-Token做鉴权非常简单它的核心思路是“登录后给你一个token后续请求带着token我用拦截器验证这个token是否有效”。RestController RequestMapping(/api/user) public class UserController { PostMapping(/login) public RString login(RequestBody LoginRequest request) { User user userService.getOne(new LambdaQueryWrapperUser() .eq(User::getPhone, request.getPhone())); if (user null) { // 未注册自动注册 user register(request.getPhone()); } // Sa-Token 登录 StpUtil.login(user.getId()); String token StpUtil.getTokenValue(); return R.ok(token); } GetMapping(/info) public RUserVO info() { Long userId StpUtil.getLoginIdAsLong(); User user userService.getById(userId); return R.ok(convertToVO(user)); } }这里有一个容易被忽略的问题token一定不能只放在前端后端的拦截器必须排除登录、注册等公开接口。用Sa-Token的注册拦截器可以轻松搞定Configuration public class SaTokenConfigure implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle - StpUtil.checkLogin())) .addPathPatterns(/**) .excludePathPatterns(/api/user/login, /api/user/register, /api/shop/**); } }3.4 下单流程与库存防超卖下单是整个系统的高潮部分也是并发问题最容易暴露的地方。正常的流程是用户从购物车选中商品提交订单后端校验门店和菜品状态计算总价扣减库存生成订单记录清空购物车返回订单号然后发起支付。这块最容易翻车的是库存扣减如果直接用“先查库存判断够不够再减库存”的逻辑在高并发场景下会发生超卖。举个例子某个菜品库存只剩1份两个用户同时下单两个请求都查到库存为1都判断“够”然后都执行扣减结果库存变成了-1卖了2份。解决办法是在SQL层面加条件int updated dishMapper.updateStock(dishId, count); // 对应SQL: UPDATE dish SET stock stock - #{count} WHERE id #{dishId} AND stock #{count} if (updated 0) { throw new ServiceException(菜品库存不足); }这段代码本质上利用了数据库行锁同一时刻只有一个请求能成功执行stock count这个条件判断其余请求更新行数为0直接返回库存不足。这是最简单也最稳妥的防超卖方案比用代码加锁简单而且效率高。下单方法记得加上Transactional注解保证“扣库存生成订单清购物车”这三个操作要么全部成功要么全部回滚否则会出现订单没有生成但库存已经减少了的情况。3.5 定时任务取消超时订单用户下单后迟迟不支付订单会一直占着库存影响其他用户下单。常规做法是给订单加一个“创建时间”字段然后定时扫描超过30分钟仍未支付的订单将其置为已取消同时回补库存。Component public class OrderTimeoutTask { Resource private OrderMapper orderMapper; Scheduled(cron 0 */1 * * * ?) public void cancelTimeoutOrders() { LocalDateTime deadline LocalDateTime.now().minusMinutes(30); ListOrder orders orderMapper.selectList(new LambdaQueryWrapperOrder() .eq(Order::getStatus, OrderStatus.UNPAID.getCode()) .lt(Order::getCreatedAt, deadline)); for (Order order : orders) { order.setStatus(OrderStatus.CANCELLED.getCode()); orderMapper.updateById(order); // 回补库存 restoreStock(order); } } }Scheduled配合cron表达式每1分钟扫一次逻辑简单够用。注意在启动类上加EnableScheduling注解否则定时任务不生效。这个定时任务也是答辩时老师喜欢问的点“如果服务重启扫描会不会漏掉订单”答案是可以扫描任务启动时先把当前所有未支付订单的最后支付时间检查一遍作为兜底。4. 前端与部署管理后台、H5端、Nginx部署4.1 前端技术选型毕设前端建议做成前后端分离管理后台用Vue 3 Element Plus用户端用微信小程序。管理后台负责管理门店、菜品、订单用户端就是学生打开点餐的小程序。前后端通过HTTP接口交互所有数据格式统一为JSON。管理后台的界面不用自己从零写直接用现成的后台模板能节省大量时间。Vue 3配Element Plus可以拼出来一整套管理界面。用户端的H5页面如果不想写小程序也可以用Vue 3 Vant组件库快速搭一个移动端页面演示效果一样能达到。4.2 跨域处理与接口联调前端页面跑在localhost:5173后端接口跑在localhost:8080浏览器会拦截跨域请求。解决跨域的方法很多简单点的在后端写一个配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这里有个细节如果用了allowCredentials(true)allowedOrigins不能直接写*要写allowedOriginPatterns(*)否则跨域请求会被浏览器拦截。接口联调建议用Apifox这类工具把接口文档、Mock、调试整合在一起比Postman更适合团队协作。联调时要把后端返回的统一响应体定义好例如{ code: 200, message: success, data: {} }前端根据code判断业务是否成功而不是根据HTTP状态码这是企业里的标准约定。4.3 服务器部署毕设验收前通常要把系统部署到服务器上让老师通过公网访问。推荐用阿里云或腾讯云的轻量应用服务器2核2G的配置足够。部署架构图很简单Nginx监听80/443端口静态资源由Nginx直接托管接口请求反向代理到Spring Boot的8080端口。MySQL和Redis直接装在服务器上即可小项目用不着Docker。Nginx的关键配置server { listen 80; server_name yourdomain.com; # 前端静态页面 location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } # 后端接口 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }部署的坑主要在防火墙和数据库端口上。服务器安全组必须放行80和443端口生产环境绝对不能把3306端口暴露到公网否则十分钟就能被扫描机器人攻击。数据库连接串里的密码务必使用环境变量或配置文件外置不要硬编码在代码里。5. 性能优化与并发场景处理5.1 缓存热点数据外卖系统里门店列表、菜品列表是访问频率最高的数据每次请求都穿透到数据库完全没有必要。我通常在服务层加一层Redis缓存public ListDishVO getDishListByShopId(Long shopId) { String key shop:dish: shopId; String cache redisTemplate.opsForValue().get(key); if (cache ! null) { return JSON.parseArray(cache, DishVO.class); } ListDishVO list dishMapper.selectByShopId(shopId); redisTemplate.opsForValue().set(key, JSON.toJSONString(list), 30, TimeUnit.MINUTES); return list; }商家在后台更新菜品后要主动删除缓存否则用户看到的还是旧数据。这个“缓存更新策略”也是答辩常见问题你需要说清楚缓存什么时候失效、什么时候更新。5.2 秒杀式场景的订单优化外卖平台上偶尔会有“限量特价菜”“整点抢券”之类的活动一瞬间大量请求涌入。前面讲的SQL条件防超卖方案能保证不超卖但当并发量极高时数据库的行锁竞争会很激烈接口响应变慢。更进阶的做法是引入MQ比如把下单请求写入RabbitMQ队列由消费者串行处理订单把高并发请求削峰填谷。毕设阶段不一定要接MQ但可以把思路写在论文里说明“如果有高并发场景我会用消息队列做异步化处理”。老师听到这个一般就不会再追问下去了因为说明你思考过扩展方向。6. 毕设开发中常见的坑6.1 常见问题速查表开发过程中有些问题几乎每个学生都会遇到整理成表格方便排查问题报错表现解决思路中文乱码响应中文显示为???datasource url加characterEncodingutf8接口返回前检查响应头Content-Type数据库连接失败Communications link failure检查MySQL服务是否启动密码是否正确端口是否开放Maven依赖下载慢jar包一直找不到配置阿里云镜像IDEA里选择正确的Maven配置前端跨域被拦Access-Control-Allow-Origin检查CorsConfig注意allowCredentials和allowedOriginPatterns分页不生效返回全量数据使用MyBatis-Plus时引入分页插件MybatisPlusInterceptor定时任务不执行无任何日志启动类是否加了EnableSchedulingRedis连接超时redis.clients.jedis.exceptions检查redis-server是否启动bind配置是否是127.0.0.1端口被占用Port 8080 was already in uselsof -i:8080 找到进程并kill或改端口6.2 答辩老师的关注点答辩本质上不是考你代码写了多少行而是考“你的系统如何设计、为什么这么做”。我把老师最爱问的几个问题列一下提前准备就好为什么选Spring Boot答快速构建、生态成熟、配置简化、适合中小型项目。订单状态如何保证不乱答状态机约束 后端校验。如何防止超卖答数据库乐观锁思路SQL条件更新配合事务回滚。缓存怎么保持一致性答写操作后主动删除缓存下一次读操作重新加载。购物车为什么存Redis不存MySQL答高频操作、性能好、TTL可以自动过期清理减少DB压力。这些问题的回答逻辑并不难你只要在开发时真正用了这些方案脑子里有过印象就能答得上来。怕的是照着别人的代码粘贴复制什么都不改老师一问三不知那就很容易被挂。做这个项目最大的体会是Spring Boot本身并不难难的是把业务想清楚再动手。很多同学一上来就想写代码结果数据库字段建得乱七八糟逻辑写到一半推倒重来。按照这篇文章的顺序——先设计业务、再建表、再写后端、再搭前端、最后部署每一步都有明确的产出物整条线走下来你会发现自己收获的不仅是一个能演示的系统更重要的是理解了从零到一做一个Web产品的完整过程。最后再分享一个小技巧开发时保持Git提交习惯每完成一个小功能就提交一次出问题时可以快速回退面试时还能拿出项目记录展示你的工程素养。
返回列表