
1. 为什么选Java Vue做校园外卖点餐需求盘点与收益分析1.1 校园外卖点餐场景的核心痛点大学校园里点外卖这件事做过的人都知道跟社会外卖完全是两套逻辑。社会外卖靠骑手在外面跑但校园里往往管得严外卖送到校门口就进不来了或者只能放到某个集中取餐点高峰期找餐跟开盲盒一样。再加上校园里食堂档口、周边小商铺林立订单分散、品类复杂、价格敏感度高如果用通用外卖系统去套体验会非常割裂。所以校园外卖点餐系统本质上是给一个相对封闭的小生态做一套专属解决方案。用户侧是学生需要快速浏览附近商家、下单、支付、看配送状态商家侧是食堂档口或校园周边店需要接单、备餐、出餐管理侧是平台运营方或学校后勤需要审核商家、管理订单、统计数据。这个业务规模不大但业务流程非常完整——用户、商品、购物车、订单、支付、配送、评价全链路都覆盖了是典型的毕业设计或课程设计级项目。从实际开发角度看这类项目的难点不在于某个算法而在于“业务链路长、角色多、状态变化频繁”。把这条链路理清楚代码怎么写都不会太乱理不清楚后面每加一个功能都要返工。1.2 功能模块拆解三类角色、四套核心流程这套校园外卖点餐系统按照角色划分核心用户有三类学生点餐人、商家档口/店铺、平台管理员。围绕这三类角色业务闭环可以拆成四套核心流程点餐流程学生浏览商家 → 浏览菜品 → 加入购物车 → 确认下单 → 支付模拟支付或预支付 → 查看订单状态。接单出餐流程商家收到新订单 → 接单/拒单 → 出餐 → 通知配送/自取。配送流程骑手或配送员接单 → 取餐 → 送达 → 完成订单。管理流程管理员管理用户、审核商家、上下架菜品、查看运营数据、处理售后/退款。这四套流程不是孤立的而是靠订单状态机串起来的。订单从“待支付”到“已支付”到“商家已接单”到“配送中”到“已完成”每一步都是一次状态变更每一次状态变更都要更新数据库中的订单记录同时可能触发消息通知或推送。把状态机的流转规则定义清楚是整个系统的地基。1.3 这种前后端分离结构适合学生项目的原因很多学生拿到题目之后第一反应是我用JSP Servlet MySQL也能做为什么非要Java Vue确实能做但体验完全不一样。首先是职责分离。前端用Vue做页面渲染和交互后端用Spring Boot提供纯JSON接口两边可以并行开发调试时也不用像JSP那样改一下页面就要重启应用。对于课设而言这种分离结构交上去之后老师看代码逻辑时也更直观——后端就专注处理数据前端就专注展示。其次是技术栈的含金量。Java Spring Boot MyBatis Plus Vue这套组合跟企业里中小型管理系统的实际技术栈高度接近。你用它做出来的项目写在简历上不需要额外解释“这是什么”面试官一看就懂。相比之下JSP那个年代的方案放到现在HR和面试官大概率只会觉得你在应付。再一个是开源生态。Vue有Element UI这类成熟组件库后台管理界面和移动端H5界面都能快速搭建不用自己手写一堆CSS和弹窗逻辑。Spring Boot有官方和社区的大量Starter分页、鉴权、参数校验都有现成方案能节省大量造轮子时间。这篇博文后面的章节我会把整个系统的核心设计、数据库表结构、启动步骤和踩坑记录都过一遍项目代码、数据库脚本和文档一起配合着看照着就能跑通。2. 技术栈选型与项目目录结构设计2.1 后端Spring Boot如何支撑业务后端选型上Spring Boot是目前Java做这类系统的主流方案没有太大悬念。但框架内部怎么组织很多新手容易走弯路。我在做这套系统的时候后端选了Spring Boot 2.7.x MyBatis Plus MySQL 8.0 JWT。Spring Boot负责接口提供和依赖管理MyBatis Plus用来做ORM和数据访问JWT承担登录鉴权Redis的问题后面再说——如果只是为了课设跑通可以先不用Redis直接JWT数据库就行。为什么用MyBatis Plus而不是原生MyBatis这个在答辩时也经常被问到。原生MyBatis需要自己写大量的Mapper XML单表增删改查都要手工写SQL效率很低。MyBatis Plus内置了通用Mapper和通用Service单表操作直接调用内置方法比如selectById、saveOrUpdate、page省掉80%的单表SQL把精力集中在业务逻辑上。多表关联查询再手写XML也完全不冲突。Spring Boot的分层结构我建议这样分controller接收请求、参数校验、返回统一结果。service业务逻辑层事务控制都放在这一层。mapper数据访问层继承MyBatis Plus的BaseMapper。entity数据库表对应的实体类。dto/vo接口接收参数用的DTO、返回给前端用的VO避免直接暴露数据库实体。config配置类如CORS跨域配置、拦截器注册、MyBatis Plus分页插件配置。common统一返回结果、全局异常处理、工具类。interceptorJWT登录拦截器。这套分层不复杂但足够规范。交上去的代码如果分层清晰老师一眼就能看出你是懂工程实践的而不是把几千行代码全塞在Controller里。2.2 前端Vue Element UI怎么搭管理界面前端用的是Vue 2 Vue Router Vuex Axios Element UI。这里解释一下为什么不建议Vue 3不是Vue 3不好而是Vue 2的生态更稳Element UI对Vue 2的支持最成熟网上的资料和报错解决方案也最多。对于课设项目稳定第一选Vue 2能把踩坑时间压到最低。前端分成两个端会更清晰用户端H5风格页面面向学生包括商家列表、菜品列表、购物车、订单结算、订单状态、个人中心。用Vant组件库或手写移动端风格都可以想省事就直接用Element UI加一点响应式样式。管理端后台风格页面面向商家和管理员包括商家管理、菜品管理、订单处理、用户管理、数据统计。前端最核心的部分不是页面长什么样而是Axios封装和路由守卫。Axios需要统一封装请求拦截器和响应拦截器请求时在Header里带上Token响应时统一处理401未登录状态和业务错误码。路由守卫则负责页面访问控制——未登录不能进个人中心商家账号不能进管理员页面这些逻辑都写在router.beforeEach里。2.3 目录与包结构的关键类职责划分前端目录上我习惯把项目分成视图层和公共模块src/ api/ // 所有接口请求按模块拆分 assets/ // 静态资源 components/ // 公共组件 router/ // 路由配置 store/ // Vuex状态管理 views/ // 页面组件 user/ // 学生端页面 shop/ // 商家端页面 admin/ // 管理员页面 utils/ // 封装工具request.js, auth.js后端包结构前面已经说了再补充一个容易被忽略的点统一返回结果。我建议定义一个Result类包含code、message、data三个字段所有Controller接口都返回Result.ok(data)或Result.error(msg)这样前端Axios拦截器只需要针对code做一次判断而不需要每个页面单独处理错误状态。这个设计在答辩时提出来会很有说服力。3. 数据库设计订单业务最关键的七张表与字段取舍3.1 实体关系梳理从业务到表数据库是这类系统的核心表设计不好后面写代码会各种别扭。校园外卖点餐系统的核心实体我梳理下来一共七张表用户表、商家表、菜品分类表、菜品表、购物车表、订单表、订单明细表。如果需要简化可以再加一张配送地址表但学生用户一般可以把默认地址合并到用户表里看需求复杂程度。实体关系上用户和订单是一对多商家和菜品是一对多菜品分类和菜品是一对多订单和订单明细是一对多。购物车表比较特殊它本质上是“用户-菜品-数量”的中间关联表可以关联用户ID和菜品ID也可以用cart_id做独立主键。我建议独立主键因为独立主键在“修改购物车数量”“删除购物车项”时操作更简单不用拼联合主键。3.2 核心表结构与字段设计细节用户表t_user主要字段id主键建议使用BIGINT自增或者用雪花ID如果是课设用自增就够了。username用户名唯一索引。password密码存BCrypt加密后的串不要存明文。phone手机号。role角色0学生1商家2管理员。avatar头像。status状态启用/禁用。create_time/update_time创建与更新时间。菜品表t_dish主要字段idshop_id所属商家ID外键逻辑关联。category_id所属分类ID。name菜品名称。price价格用DECIMAL(10,2)不用FLOAT否则浮点误差会在对账时暴露出来。image图片地址。description描述。stock库存。status上架/下架状态。sales销量可以做排序展示。订单表t_order是整个系统最核心的表字段相对多idorder_no订单编号需要唯一可以用日期随机数生成。user_id下单用户ID。shop_id商家ID。total_amount订单总金额减掉优惠、加上配送费之后的结果。status订单状态0待支付1待接单/已支付2商家已接单3配送中4已完成5已取消6申请退款。address收货地址快照保存防止用户之后修改地址影响历史订单。remark订单备注。create_time/pay_time/complete_time各个时间节点。订单明细表t_order_detailidorder_id订单ID。dish_id菜品ID。dish_name菜品名称快照。dish_image菜品图片快照。price下单时的单价快照。quantity数量。subtotal小计金额。3.3 为什么订单明细要做“快照”而不是直接关联菜品表这里有一个很关键的设计点也是很多新手容易忽略的订单明细里的dish_name、dish_image、price都必须冗余存储而不能在展示订单时再去JOIN t_dish查菜品表。原因很简单商家可以随时修改菜品的价格、名称、图片甚至可以删掉菜品。如果订单明细里只存一个dish_id用户过了三个月查历史订单看到的价格和名称可能跟当时买的时候完全不一样这就是典型的“订单漂移”问题。快照模式下用户看到的就是他当时下单那一刻的菜品信息永远不变。同理订单表里的address字段也必须冗余存储。用户改了默认地址历史订单上的收货地址不能被改掉所以下单时要把地址原样拷一份到订单里。快照这个思路不仅仅用在外卖系统电商系统的订单中心也是这么设计的答辩时能说出这个设计意图绝对是一个加分项。还有金额字段DECIMAL(10,2)是标配。不要用DOUBLE算钱Java侧也不要直接用double做金额计算用BigDecimal每一步运算都考虑精度这几乎是企业开发的硬性要求。4. 从零跑通项目环境准备与启动步骤4.1 后端环境JDK、Maven、MySQL怎么配拿到项目之后第一步不是急着写代码而是把环境跑通。根据这套系统的技术栈后端跑起来需要三样东西JDK 1.8或11、Maven 3.6、MySQL 5.7或8.0。JDK安装时要注意环境变量。JDK11和JDK1.8的目录结构略有差异但如果用Maven其实不一定要手动配JAVA_HOME很多IDE会自动检测JDK。不过为了命令行操作方便还是建议手动配置一下。Maven核心是配置国内镜像。不配置镜像的话Maven中央仓库下载依赖动辄卡十分钟很多新手以为系统坏了。在settings.xml里加阿里云镜像mirror idaliyunmaven/id mirrorOfcentral/mirrorOf nameAliyun Maven Mirror/name urlhttps://maven.aliyun.com/repository/public/url /mirrorMySQL安装时要特别注意字符集和时区。字符集要选utf8mb4而不是utf8因为utf8在MySQL里最多只支持3个字节存储用户备注里可能出现的某些生僻字或特殊符号时会报错。连接数据库的URL里一定要带时区参数否则Java连接MySQL 8.0时大概率会遇到时区报错spring.datasource.urljdbc:mysql://localhost:3306/campus_takeout?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai spring.datasource.usernameroot spring.datasource.password你的密码4.2 导入数据库脚本与初始化数据数据库脚本通常在源码目录下文件名一般是sql/campus_takeout.sql或类似的名称。用Navicat或命令行执行导入都行。导入之后一定要检查三件事是否生成了所有表。是否包含初始管理员账号比如admin。商家账号和普通用户账号是否有样例数据。如果脚本里自带测试数据先不要删。没有测试数据你启动系统之后点什么都看不到排查问题时不知道该怪前端还是后端还是数据库。样例数据就是一套仪表盘能帮你确认系统运行情况。导入完成之后检查后端配置文件里的数据库账号密码是否跟本地一致。大部分项目里application.yml或application.properties写的是root/123456如果你本地的MySQL密码不一样需要改掉否则启动后端时数据源初始化会直接报错。4.3 前端环境Node与依赖安装前端需要Node.js建议用14.x或16.x版本。Vue 2项目装依赖用npm install时偶尔会因为依赖版本和Node版本不匹配报错这时候优先检查Node版本而不是乱改package.json。安装依赖cd frontend npm install如果默认源网速慢就用淘宝镜像npm config set registry https://registry.npmmirror.com依赖装完之后启动前端开发服务器npm run serve默认端口一般是8080但很多项目的后端接口前缀是/api前端的请求路径可能拦截在8081或9528这个看项目的Vue配置文件vue.config.js里通常有module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } };有这个代理配置之后前端代码里请求路径写/api/user/login开发时就会被代理转发到后端的http://localhost:8081/api/user/login既解决了跨域问题也让前后端联调时不需要关注后端实际地址。4.4 启动顺序与验证方式启动顺序有讲究原则是先数据库再后端最后前端。数据库确认连接正常后启动后端服务。IDEA里运行主类看到控制台输出Started Application in X.XX seconds后端就是起来了。这里有一个常见坑如果你的项目里配置文件指定了Redis连接但本地没有装Redis启动时会一直报Redis连接失败。不要慌去配置文件里看Redis相关配置如果是localhost且没设密码本地装一个Redis或者暂时注释掉Redis依赖和自动配置即可。后端启动完成后可以用Postman或Apifox直接测一个接口比如登录接口POST http://localhost:8081/api/user/login Content-Type: application/json { username: student1, password: 123456 }正常情况下会返回一个Token。能拿到Token说明后端接口、数据库连接、账号密码全部正常。之后启动前端浏览器访问http://localhost:8080用账号登录能看到页面数据整套系统就算跑通了。5. 核心业务模块的实现逻辑详解5.1 登录鉴权JWT的签发与拦截器校验校园外卖点餐系统里有三种角色但登录流程可以统一设计。用户提交用户名密码后后端校验成功就签一个JWT返回给前端前端存在localStorage里之后每次请求都带上这个Token。JWT的结构包含三个部分Header、Payload、Signature。签发时在Payload里放userId和role用服务端配置的密钥做签名// 基于Java实现JWT生成示例仅展示核心逻辑 String token Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim(role, user.getRole()) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();拦截器校验Token时核心是排除掉登录、注册等白名单路径然后从请求头里取出Authorization解析JWT解析失败或过期就返回401不进入Controller。这里要注意一个对新手来说非常容易踩的坑Token过期时间不要设置成永久课设虽然不用太严格但设置7天有效期已经足够了面试时如果被问到Token有效期也能说明理由。5.2 下单流程购物车、订单、库存扣减的事务处理下单流程是整套系统里逻辑密度最高的地方也是最容易出现数据不一致的地方。完整流程是这样的前端把当前购物车ID列表和收货地址发给后端。后端收到请求后查询购物车及其关联的菜品信息。计算订单总金额。校验菜品状态与库存。生成订单主记录状态为“待支付”。生成订单明细记录。清空对应购物车记录。返回订单ID给前端前端跳转到支付页面。这个流程里最关键的问题在于第3步到第6步之间有多个写操作要么全部成功要么全部失败。如果订单生成了、库存没扣就会出现超卖如果库存扣了、订单没生成就会出现资金不对账。所以必须使用数据库事务把这些操作包在一个Transactional里任何一步抛异常就整体回滚。对库存的处理我建议用乐观锁方案。在菜品表加一个version字段扣库存时执行这样的SQLUPDATE t_dish SET stock stock - #{quantity}, version version 1 WHERE id #{dishId} AND stock #{quantity} AND version #{version}这条SQL同时完成了“扣减”“校验库存充足”“校验版本号”三件事。如果影响行数为0说明库存不足或数据已被其他请求修改立刻抛出业务异常回滚。乐观锁既能防止超卖也不会像悲观锁那样把整条记录锁住影响并发性能。校园外卖并发量不大但面试官非常喜欢问这个场景。5.3 订单状态机与商家、配送端接单订单状态是整个系统的业务骨架。我在代码里维护了一个状态的流转规则不允许随意跳状态。比如只有“待支付”的订单才能取消只有“已支付”的订单才能被商家接单只有“配送中”的订单才能标记完成。商家端接单的核心接口Transactional public void acceptOrder(Long orderId, Long shopId) { // 校验订单归属 Order order orderMapper.selectById(orderId); if (order null || !order.getShopId().equals(shopId)) { throw new BizException(订单不存在); } // 校验当前状态是否允许接单 if (order.getStatus() ! 1) { throw new BizException(当前订单状态不允许接单); } // 更新状态 order.setStatus(2); orderMapper.updateById(order); }这里有一个最佳实践状态流转不要散落在各个Service里而是定义一个状态机工具类或枚举集中维护每个状态的可达状态集合。比如public enum OrderStatusEnum { WAIT_PAY(0, 待支付), PAID(1, 已支付/待接单), ACCEPTED(2, 商家已接单), DELIVERING(3, 配送中), COMPLETED(4, 已完成), CANCELED(5, 已取消), REFUNDING(6, 申请退款); }用枚举维护状态的好处是代码里不会出现神秘的魔法数字而且可以定义每个状态允许跳转到哪些状态的方法后续加状态时只要改一处。5.4 前端购物车与订单页的数据交互前端购物车推荐用Vuex管理同时同步一份到localStorage。为什么这么做Vuex负责页面间共享数据刷新页面后Vuex数据重置再从localStorage恢复这样用户刷新购物车页面时商品不会突然消失。下单时前端要展示两个关键信息总金额和配送费。配送费的规则可以简单做比如满30免配送费不满30收5元。这个规则前后端都要实现一遍吗不需要。前端展示的配送费只是预估真实计算以后端返回为准前端没有权力自己决定用户付多少钱。这个逻辑在处理退款、优惠时尤其重要凡是跟钱相关的计算一律以后端为准。6. 这套系统最容易踩的坑我的排查记录6.1 跨域问题前端访问后端接口报错跨域是前后端分离项目的第一个拦路虎。现象很典型前端页面能打开但登录请求发出去之后浏览器控制台报No Access-Control-Allow-Origin header is present on the requested resource。解决方案有几种最推荐的是给后端加一个全局CORS配置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)和allowedOriginPatterns(*)要同时使用如果只写allowedOrigins(*)在携带Cookie或浏览器严格模式下会报错。加了配置之后OPTIONS预检请求也要能通过所以allowedMethods里不要漏掉OPTIONS。如果项目里配了拦截器还要注意CORS配置和拦截器的执行顺序。拦截器如果拦了OPTIONS请求并返回了401即使CORS配置正确也白搭。我的做法是拦截器里直接放行OPTIONS请求这是排查这类问题最关键的一步。6.2 数据库时间差8小时两个层面都要检查用户的订单创建时间跟本地时间差了8个小时这是老生常谈但永远有人踩的坑。第一个层面是数据库连接URLserverTimezone要设成Asia/Shanghai。第二个层面是MySQL服务器的时区设置可以执行SET GLOBAL time_zone 08:00; SET SESSION time_zone 08:00;但这种方式重启MySQL后可能失效。更稳妥的方式是修改MySQL配置文件在[mysqld]节点下加default-time-zone 08:00改完重启MySQL生效。如果你在本地跑课设只是临时验证执行前两条SQL也够用。第三个层面是Jackson反序列化如果后端返回的JSON时间格式是数组而不是yyyy-MM-dd HH:mm:ss在前端显示就会很怪。可以在配置文件中统一设置spring.jackson.date-formatyyyy-MM-dd HH:mm:ss spring.jackson.time-zoneGMT86.3 图片上传成功但前端不显示菜品图片上传到本地磁盘数据库里存的是/upload/xxx.jpg前端把地址拼在src属性里结果图片404。原因通常是后端没有把这个上传目录映射成静态资源路径。在Spring Boot中需要配置虚拟路径映射把/upload/**这个URL前缀映射到实际存放文件的磁盘目录Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: D:/upload/); } }这是一定要做的不做的话数据库里存的路径代表的是一个后端根本不存在的URL。另外注意Windows和Linux的路径分隔符差异代码里尽量不要写死绝对路径用配置文件来维护会更灵活。6.4 MyBatis Plus分页插件不生效用了MyBatis Plus之后很多人发现分页查询返回的总记录数和数据都不对或者分页完全没生效。原因基本都很雷同没有配置分页插件拦截器。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这个配置是必须的。不配这个拦截器Page对象传进去之后框架会在底层执行不带LIMIT的查询数据量和分页逻辑全乱套。此外分页时total默认会执行count查询如果数据量很大可以考虑手动指定count查询SQL但校园外卖这个量级完全不用在意。6.5 前端刷新页面404以及Token过期后交互混乱前端用的是Vue Router的history模式时刷新一个二级路由页面比如/order/detail/1001会返回404原因是后端没有对history模式做回退处理。最简单的方案是改用hash模式但如果你坚持用history模式就要在部署的Nginx或后端加try_files规则让所有非静态资源请求都落到index.html。Token过期后的处理也是很多人忽略的。我建议在Axios响应拦截器里统一拦截当后端返回401时清除本地Token、跳转登录页、提示“登录已过期请重新登录”。如果不做统一处理用户会在页面上看到各种不可理解的报错做出奇怪的操作最终留下一堆脏数据。7. 二次开发与答辩让这个项目真正属于你7.1 做出彩之前先想清楚加什么网上能下载到的相关项目源代码很多真正的问题是代码是别人的你怎么让它变成你自己的课设或毕业设计答辩时老师会追着项目细节提问如果连核心业务是怎么跑的都没弄明白一问就露馅。我的建议是不要贪多集中做好一两个“亮点功能”。比如给菜品模块加上“今日推荐”和“销量排行”需要用Redis做缓存热点数据。给订单模块加上超时未支付自动取消功能用Spring Boot的Scheduled定时扫描待支付订单超时30分钟自动取消。给管理员端加上简单的数据看板用ECharts展示每日订单量、营业额、热门菜品Top10。这三个方向里我比较推荐第二个。定时任务自动取消订单涉及状态机的边界处理比如“用户正在支付订单被定时任务取消了怎么办”这类细节在答辩时很好展开能体现你确实思考过业务。7.2 必背的几个面试追问点与思路这个项目写到简历上面试官大概率会问几个方向提前准备一下订单超卖怎么防止答乐观锁CAS扣库存同时校验stock quantity配合数据库事务。为什么用MyBatis Plus答单表CRUD复用内置方法多表SQL手写XML兼顾开发效率和SQL可控性。登录状态怎么维持的JWT的优势和缺点是什么答无状态、适合分布式但注销和过期控制比较麻烦。下单过程如果支付超时怎么处理答引入订单超时关闭机制可以是定时任务正式项目可以用消息队列延迟消息。前端Vue的响应式原理是什么答Vue 2用Object.defineProperty对每个属性做getter/setter劫持Vue 3用Proxy代理整个对象这也是Vue 3性能提升的原因之一。7.3 部署上线时的一些实用建议如果不想只停留在本地跑通想把这套系统部署到服务器上展示有几个要点可以注意。云服务器选择最基础的学生机就够后端用Maven打包成jar包然后用nohup java -jar后台运行前端用npm run build打包后把dist目录交给Nginx托管需要把API请求代理到后端地址Nginx配置大致是server { listen 80; server_name 你的域名; root /usr/share/nginx/html; index index.html; location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }部署过程中最容易出问题的是数据库连接地址和图片上传路径。数据库连接不要写localhost改成服务器的内网IP或公网IP图片上传路径要确保目录有写权限否则上传菜品图片会一直失败。我在跑这类项目的实际感受是分布式、高并发这些概念离课设还很远真正影响项目质量和答辩评价的永远是基础功——表结构设计是否合理、状态流转是否清晰、事务边界是否明确、接口返回是否规范。把这些细节做好这个校园外卖点餐项目拿出去就是一件完整的作品。