ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot校园外卖系统设计与实现全攻略:从架构到答辩

Spring Boot校园外卖系统设计与实现全攻略:从架构到答辩 简介Spring Boot校园外卖服务系统完整项目资源面向计算机专业学生、毕业设计人员及Java后端开发者。系统涵盖用户、商家、订单、配送四大核心模块涉及Spring Data JPA持久化、MySQL数据库设计、Spring Security权限控制并集成Docker容器化部署与JUnit/Mockito测试方案。压缩包共793个文件大小28.84MB以117个Java后端代码、62个Vue前端组件、157个JavaScript脚本为主体辅以50个CSS样式表、38个HTML页面、14个XML配置及项目结构化文档还包括部署依赖与设计素材等资源。现已吸引41人浏览学习适合需要参考完整前后端分离项目、学习业务实现或进行二次开发的读者。资源附带数据库建表SQL与项目部署脚本可结合配套说明快速理解系统架构与业务流转逻辑。1. 这个标题在说什么不只是一份毕设代码而是一整套校园外卖业务链拿到“springboot项目校园外卖服务系统设计与实现.zip”这个压缩包的人绝大多数是准备毕业设计或课程设计的在校生。但很多人解压之后第一反应是“怎么跑起来”然后被一堆报错劝退。我做过这类系统的全流程开发可以明确告诉你这个项目的分水岭不在代码量而在订单状态的流转设计——订单状态没理清课时设计答辩时老师追问三个问题你就能翻车。这套系统本质上是一个多角色、多终端的业务闭环学生下单、商家接单出餐、配送员抢单送达、管理员兜底。表面是 Spring Boot 的增删改查实际要处理的是并发抢单、超时取消、订单状态机这类真实业务问题。适合谁适合想通过一个完整项目把 Spring Boot、MyBatis-Plus、Vue、Redis 串起来的人也适合需要一份能讲清楚“为什么这么设计”的答辩项目的人。下面按我自己的落地路径拆开讲。2. Spring Boot 校园外卖的骨架先把技术栈和模块边界钉死2.1 技术选型为什么是 Spring Boot MyBatis-Plus Vue而不是别的组合校园外卖系统本质是一个典型的管理信息系统加上一点即时配送的实时性要求。技术栈的选择不是越新越好而是要看“答辩能不能讲清楚”和“三个月后你自己还能不能改”。常见的做法是 Spring Boot 做后端接口Vue 做前端页面MyBatis-Plus 做 ORM 框架。选 MyBatis-Plus 而不是 JPA理由是校园外卖的查询场景非常贴近 SQL 思维订单表要按时间、状态、配送员多个条件组合筛选JPA 的 Specification 写起来非常绕而 MyBatis-Plus 的 LambdaQueryWrapper 一行代码就能拼出动态查询。分页插件也是现成的PageT分页对象直接返回给前端省掉手写 PageHelper 的配置。这里要提一下 Spring Boot 自动装配原理。刚接触 Spring Boot 的人经常困惑“为什么我没配 Bean 也能用 RedisTemplate”其实就是spring-boot-autoconfigure里的条件注解在起作用——类路径上有RedisConnectionFactory就自动创建 RedisTemplate。理解了这个后面遇到“依赖没引全导致自动装配失败”的报错就不会慌。Vue 这边注意不要一上来就上 Vue3 Vite TypeScript 全家桶。校园外卖这种规模的系统Vue2 的 Options API 或者 Vue3 的 Composition API 都够用关键是选自己熟练的。如果你是为了答辩选 Vue2 反而更好讲因为 Element UI 的表格和表单组件直接拖拽式使用业务代码更直观。2.2 拆模块用户、商家、订单、配送边界画在哪模块拆分决定了数据库设计和接口划分这是整个项目里最不该省时间的环节。我一般把校园外卖拆成六个模块用户模块、商家模块、菜品模块、订单模块、配送模块、管理后台模块。用户模块只管学生端的注册登录、地址管理、历史订单商家模块管店铺信息、营业状态、菜品上下架订单模块是核心管下单、支付回调模拟、取消、退款配送模块管配送员抢单、送单状态回传管理后台模块管用户封禁、商家审核、数据统计。每个模块单独建 Controller、Service、Mapper 包不要学网上那种所有 Controller 堆在一个包里的写法——答辩老师看到你项目结构侧面就知道你有没有工程意识。这里有个关键设计决策用户端和管理后台要不要拆成两个 Spring Boot 工程不要拆。单工程 多模块包结构就够了拆成两个工程意味着要处理两套登录态、两套拦截器、两套部署流程工作量翻倍但答辩不会加分。2.3 数据库设计五张核心表的关系与字段陷阱数据库设计直接决定了后面所有业务代码的写法。我见过太多人把订单表设计成“一张表装天下”——用户信息、菜品信息、地址全部冗余在订单表里结果统计商家销量时 SQL 写得像天书。最少需要这五张表用户表t_user、店铺表t_shop、菜品表t_dish、订单表t_order、订单明细表t_order_item。配送信息我建议单独一张t_delivery表而不是塞进订单表。订单表是重点有几个字段必须设计好字段名类型说明order_statustinyint状态机核心字段0待支付 1已支付 2备餐中 3待配送 4配送中 5已完成 6已取消pay_statustinyint0未支付 1已支付和 order_status 分开维护cancel_reasonvarchar记录取消原因便于管理员排查create_time / update_timedatetimeMyBatis-Plus 自动填充我的血泪经验是千万不要把订单状态和支付状态混成一个字段。校园外卖系统里订单超时取消、支付回调失败都需要查历史状态混在一起时排查问题全靠猜。另外订单明细表里必须冗余菜品单价快照别去关联菜品表实时取价——商家改价后历史订单的对账就全乱了。3. 把项目跑起来的完整步骤从解压 ZIP 到首页能点单3.1 环境准备JDK、Maven、MySQL、Redis 的版本搭配先说一个最常见的翻车点Spring Boot 版本太高。很多同学下载源码时看到2.7.x或3.x就直接跑但 Spring Boot 3.x 把javax.*包全换成了jakarta.*这些代码里import javax.servlet.*的旧项目在编译期直接报红。我一般建议用 Spring Boot 2.7.x配 JDK 8 或 JDK 11兼容性最好网上的资料和踩坑记录也最全。具体环境版本见下表这是我试过最稳的组合组件推荐版本说明JDK1.8 或 11Spring Boot 2.7 最高支持 JDK 21但没必要Maven3.6.3 以上用 IDEA 内嵌的也行MySQL5.7 或 8.0注意 8.0 的驱动要com.mysql.cj.jdbc.DriverRedis5.x / 6.x用于 JWT 存储或缓存不用也可以先跳过Node.js14 以上前端 Vue 项目构建用这里有个玄学问题IDEA 2026 的 Spring Boot 配置入口和旧版不一样。如果你用的是新版 IDEARun/Debug Configurations里找不到Spring Boot类型时先确认项目是否被识别为 Maven 项目右键pom.xml选择Add as Maven Project启动类上有没有SpringBootApplication注解这三个都对了配置入口才会出现。3.2 数据库初始化SQL 脚本不是导入就完事解压后找到sql或db目录下的.sql文件。用 MySQL 客户端执行导入mysql -u root -p campus_takeaway.sql导入完成后必须做两件检测。第一确认数据库字符集是utf8mb4而不是utf8——订单表里如果有表情符号备注比如“多加辣️”会直接报错。第二检查表名前缀看 SQL 脚本里的表名和application.yml里配置的是否一致。spring: datasource: url: jdbc:mysql://localhost:3306/campus_takeaway?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver这个配置里最容易被忽略的是serverTimezoneAsia/Shanghai。如果不加MySQL 8.0 默认时区和 Java 的时区不一致查询时间字段会差 8 个小时订单创建时间和超时取消逻辑会一起乱掉。另外注意characterEncodingutf8mb4MySQL 8.0 的驱动已经默认 utf8mb4但显式写出来能避免某些老版本驱动的兼容问题。3.3 启动 Spring Boot 后端配置文件里的三个必须改的项application.yml里除了数据源还有三个配置项几乎每次都要改。第一个是 Redis 配置。如果项目里用了 Redis 存验证码或 tokenspring: redis: host: localhost port: 6379 database: 0 timeout: 5000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0第二个是 MyBatis-Plus 的配置。我一般会开map-underscore-to-camel-case和下划线转驼峰映射否则数据库的create_time字段映射不到 Java 的createTime属性上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第三个是端口配置。后端默认 8080但前端 Vue 开发服务器默认也是 8080我习惯把后端改成 8081server: port: 8081改完配置直接运行启动类。启动日志出现Started Application in x.xx seconds不算完一定要看到 Tomcat 端口号对应的启动记录。如果报Failed to configure a DataSource先检查pom.xml里有没有引数据库驱动依赖——spring-boot-starter-jdbc和mysql-connector-j缺一不可。3.4 启动前端Vue 项目怎么就进了 Spring Boot 的静态目录如果你拿到的项目是前后端分离结构前端单独一个目录开发时这样跑cd frontend npm install npm run dev然后访问http://localhost:8080通过代理转发到后端。代理配置在vue.config.jsmodule.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } };这里有个关键点前端请求/api/user/login代理会转发到http://localhost:8081/api/user/login所以后端所有 Controller 的 RequestMapping 都要带/api前缀否则会 404。如果是“vue打包放进springboot”的部署型项目前端跑完npm run build会生成dist目录把dist下的index.html和静态资源复制到 Spring Boot 的src/main/resources/static目录下后端启动后直接访问http://localhost:8081就能看到页面。这时候路由必须改成 hash 模式因为 history 模式在 Spring Boot 里刷新页面会 404。在router/index.js里const router new Router({ mode: hash, routes: [...] });4. 核心模块代码怎么改登录鉴权、点单下单、配送抢单4.1 登录注册JWT Token 还是 Session校园外卖该选哪个校园外卖有两个端要登录学生端和骑手端。如果用 Session 方案后端要维护会话状态部署时要配置 Session 共享对毕设项目来说太重。我推荐 JWT Redis 的双 token 方案JWT 做无状态认证Redis 里存 key 做主动失效控制。生成 token 的工具类我用jjwt版本用 0.9.1兼容性最稳// JwtUtil.java import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; public class JwtUtil { // 签名密钥生产环境必须放到配置中心或环境变量不要硬编码 private static final String SECRET campus-takeaway-secret-key; // 过期时间学生端 24 小时配送端 12 小时管理端 2 小时 private static final long EXPIRE_TIME 24 * 60 * 60 * 1000L; public static String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }这段代码的逻辑是登录成功后把用户 ID 和角色塞进 token 的声明里之后每次请求都带上这个 token后端拦截器解析 token 拿到用户身份不再查数据库。这样设计的好处是接口无状态前端只要存好 token任意一台后端实例都能处理请求。要注意的参数是EXPIRE_TIME。校园外卖场景里学生端的 token 有效期设成 24 小时比较合理——中午订餐一次、晚上订餐一次一天内不用重复登录。配送员端建议设短一点防御 token 盗用的风险。实际使用时还要在 Redis 里存一个token:userId的键值为 token续期时重新生成 token 并更新 Redis实现主动踢人下线的能力。4.2 下单与订单状态机状态流转不写对后面全是坑订单模块是整个系统最核心的部分。状态机设计可以很简单用if-else判断即可但每个状态允许转移到哪些状态必须画清楚。校园外卖订单的状态流转是待支付 → 已支付 → 备餐中 → 待配送 → 配送中 → 已完成任何状态都可以到已取消。下单接口的核心代码Transactional public Order createOrder(Long userId, Long shopId, ListCartItem items, Long addressId) { // 1. 校验店铺是否营业 Shop shop shopMapper.selectById(shopId); if (shop.getStatus() ! 1) { throw new BusinessException(店铺未营业); } // 2. 计算订单金额, 从数据库取菜品价格, 不能用前端传的价格 BigDecimal totalAmount BigDecimal.ZERO; for (CartItem item : items) { Dish dish dishMapper.selectById(item.getDishId()); totalAmount totalAmount.add(dish.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 3. 创建订单主记录 Order order new Order(); order.setUserId(userId); order.setShopId(shopId); order.setOrderNo(generateOrderNo()); order.setTotalAmount(totalAmount); order.setOrderStatus(0); // 待支付 order.setPayStatus(0); order.setCreateTime(new Date()); orderMapper.insert(order); // 4. 批量插入订单明细 ListOrderItem orderItems items.stream().map(item - { OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setDishId(item.getDishId()); orderItem.setQuantity(item.getQuantity()); orderItem.setPrice(dishMapper.selectById(item.getDishId()).getPrice()); return orderItem; }).collect(Collectors.toList()); orderItemMapper.insertBatch(orderItems); return order; } private String generateOrderNo() { return SO System.currentTimeMillis() String.format(%04d, new Random().nextInt(10000)); }这段代码里有两个关键设计。第一Transactional确保订单主记录和明细记录要么同时成功要么同时失败——下单时只要明细插入挂了但主订单成功库存和金额对不上这是最典型的血泪教训。第二价格必须从数据库取不能用前端传的值否则学生抓包改价格就能 5 毛钱买一份黄焖鸡。订单金额字段我用BigDecimal而不是double原因是 double 的二进制表示会导致0.1 0.2 ! 0.3对账时差了 0.01 元排查到怀疑人生。4.3 配送抢单并发场景下的超卖问题校园外卖的配送环节有一个最经典的并发问题同一笔订单被多个配送员同时抢到。促销系统的超卖问题在这里原样复现。最靠谱的方案是数据库乐观锁。配送单表的status字段在待抢单时值为 0抢单用更新语句带上状态条件Mapper public interface DeliveryMapper { Update(UPDATE t_delivery SET courier_id #{courierId}, status 1, accept_time NOW() WHERE id #{deliveryId} AND status 0) int grabOrder(Param(deliveryId) Long deliveryId, Param(courierId) Long courierId); }Service 层的调用逻辑public Delivery grabDelivery(Long deliveryId, Long courierId) { int rows deliveryMapper.grabOrder(deliveryId, courierId); if (rows 0) { // 影响行数为 0, 说明订单已经被别人抢走了 throw new BusinessException(手慢了, 订单已被抢); } return deliveryMapper.selectById(deliveryId); }这段 SQL 的逻辑精髓在于WHERE status 0这个条件。数据库的行锁保证了两件事一是同一时刻只有一个人能把 status 从 0 改成 1二是只要更新成功其他配送员的 update 语句条件不满足影响行数为 0。你不需要显式加synchronized或分布式锁数据库本身就替你把并发挡住了。如果你的项目里用了 Redis也可以用 Redis 分布式锁做一层前置拦截但要注意锁的过期时间要大于业务执行时间否则锁提前过期依然会出问题。我这边一般就用数据库乐观锁简单且不会引入新的故障点。4.4 定时任务超时未支付订单自动取消的 Spring 实现订单创建后 15 分钟未支付需要自动取消释放菜品库存。Spring Boot 的Scheduled注解就能实现不需要引入 Quartz。Component public class OrderTimeoutTask { Autowired private OrderMapper orderMapper; // 每 5 分钟扫描一次超时订单 Scheduled(fixedRate 300000) public void cancelExpiredOrders() { Date expireTime new Date(System.currentTimeMillis() - 15 * 60 * 1000L); ListOrder expiredOrders orderMapper.selectExpiredOrders(expireTime); for (Order order : expiredOrders) { // 状态改为已取消, 记录取消原因 order.setOrderStatus(6); order.setCancelReason(超时未支付自动取消); order.setUpdateTime(new Date()); orderMapper.updateById(order); // 恢复菜品库存 ListOrderItem items orderItemMapper.selectByOrderId(order.getId()); for (OrderItem item : items) { dishMapper.increaseStock(item.getDishId(), item.getQuantity()); } } } }这段逻辑里的关键点是扫描条件的写法selectExpiredOrders的 SQL 必须是WHERE order_status 0 AND create_time #{expireTime}不能只查create_time。因为如果用户已经手动支付了你用时间条件再去取消就会出大事故——用户付了钱订单被取消售后问题直接爆炸。fixedRate 300000这个参数的含义是任务启动后每隔 5 分钟执行一次不管上一次任务有没有执行完。如果订单量比较大扫描时间可能超过 5 分钟这时可以考虑改成fixedDelay即上次执行完再等 5 分钟。校园外卖这个量级用fixedRate就够了但要记得在定时任务类上加EnableScheduling注解才能生效。5. 避坑校园外卖 Spring Boot 项目最常见的 5 个翻车现场5.1 前端接口 404明明 Controller 写了请求就是过不去现象前端页面能打开但所有/api/*接口全部返回 404后端日志里看不到任何请求记录。原因这里排查两件事。第一后端启动类的位置不对——Application.java放在com.example.controller包下而 Controller 在com.example.campus.controller包下Spring Boot 默认只扫描启动类所在包及子包Controller 根本不会被注册。第二前端代理没生效请求直接打到了静态资源上而不是后端端口。解决把启动类移到最外层包的根上比如com.campus.Application。前端用vue.config.js的代理时在浏览器 Network 面板里看请求的 URL——如果直接是http://localhost:8080/api/xxx说明代理没起作用检查 devServer 配置是否在根节点而不是嵌套在module.exports里。5.2 跨域报错Access-Control-Allow-Origin 缺失现象前端单独跑在 8080 端口后端在 8081 端口浏览器控制台报跨域错误请求 OPTIONS 预检失败。原因前后端分离开发时前端页面发出的 AJAX 请求源是http://localhost:8080后端响应头里没有Access-Control-Allow-Origin浏览器就拦截了响应。解决在后端加一个全局跨域过滤器Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }注意allowedOriginPatterns和allowedOrigins的区别allowedOrigins不允许与allowCredentials(true)同时使用Spring Boot 新版本会直接报错。还有一种情况是配置了跨域但请求头里带了自定义 header比如 token 字段allowedHeaders必须配*或显式列出否则预检请求还是会挂。5.3 MyBatis-Plus 逻辑删除与唯一索引打架现象用户表的手机号字段设置了唯一索引用户注销逻辑删除后同一手机号再次注册报Duplicate entry错误。原因逻辑删除只是把deleted字段从 0 改成 1记录还留在表里唯一索引继续生效新插入的同手机号记录就撞了。解决三种方案按项目规模选。第一种把逻辑删除字段加进唯一索引改成uk_phone_deleted(phone, deleted)但只能插两条同手机号的记录第三次还会撞。第二种注册时先查deleted 1的同手机号记录把它物理删除再插入新记录。第三种不设唯一索引注册时先查询判断手机号是否已存在。我推荐第三种——校园外卖系统里用户量不大先查后插完全够用。5.4 JWT 过期前端页面一直是白屏或卡在登录态现象用户登录后挂机一晚上第二天点任何功能都没反应接口返回 401但前端还在旧页面。原因前端没有统一处理 401 响应。axios 请求失败时只打印了错误没有清除本地 token也没有跳转回登录页。解决在 axios 拦截器里统一处理axios.interceptors.response.use( response response, error { if (error.response error.response.status 401) { localStorage.removeItem(token); localStorage.removeItem(userInfo); window.location.href /login; } return Promise.reject(error); } );这段代码的逻辑是任何接口返回 401先清掉本地 token 和用户信息再强制跳转登录页。注意不要在拦截器里用router.push(/login)而是用window.location.href——如果用的是 hash 路由router.push可能因为路由实例还没就绪而无效硬跳转最可靠。5.5 分页数据对不上PageHelper 和 MyBatis-Plus 分页插件混用现象项目里同时引了 PageHelper 和 MyBatis-Plus分页查出来的总条数不对或者第一页和第二页数据重复。原因两个分页插件内部的拦截器冲突了。PageHelper 拦截的是Executor的 query 方法MyBatis-Plus 的分页插件也是基于拦截器实现的两个插件同时存在时执行顺序不确定SQL 被改写了两次。解决只要用 MyBatis-Plus就把 PageHelper 的依赖排除干净。只保留 MyBatis-Plus 自带的PaginationInnerInterceptorConfiguration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(100L); interceptor.addInnerInterceptor(pagination); return interceptor; } }maxLimit这个参数值得注意——不设的话前端把pageSize改成 100000 就能一次拉走全表数据对接口性能是灾难。设成 100 是合理的业务约束。6. 从“能跑”变成“能答辩”三个进阶验证手段6.1 用 Postman 跑通一条完整业务链路很多同学开发时只用浏览器点点点到答辩现场一紧张就不知道怎么演示。我建议把核心业务链路用 Postman 保存下来到时候按顺序回放比现场现敲命令靠谱得多。推荐按这条链路完整跑一遍步骤接口关键参数注册学生账号POST /api/user/registerphone, password, address登录POST /api/user/login返回 token搜索商家GET /api/shop/searchkeyword, page, size查看菜品GET /api/dish/listshopId下单POST /api/order/createshopId, items, addressId模拟支付POST /api/order/payorderId商家接单PUT /api/order/acceptorderId配送员抢单PUT /api/delivery/grabdeliveryId确认送达PUT /api/delivery/completedeliveryId查看订单状态GET /api/order/detailorderId每个步骤的响应体里我习惯记录下orderStatus的值答辩时能直接演示“状态从 0 变成 1 再变成 5”的完整流转。这套链路也是接口测试的回归用例后端代码改过之后先跑一遍比手动测试省 20 分钟。6.2 并发测试用 Jmeter 验证抢单不超卖如果配送模块做了抢单功能答辩时老师很喜欢问“并发你来多少人能撑住”。建议用 Jmeter 做一个最简单的并发测试设置 50 个线程同时抢同一笔订单断言返回结果中只有 1 个成功、49 个失败。Jmeter 的配置路径是新建线程组线程数设 50Ramp-Up Period 设 0同时发起添加 HTTP 请求填抢单接口添加查看结果树和聚合报告。跑完看后端日志确认数据库乐观锁生效——grabOrder的 update 语句有 49 次影响行数为 0。这个测试的意义不仅是验证功能正确更像是一次真实的“系统体检”。如果你发现 50 个并发请求里出现多个成功说明乐观锁没生效回去检查WHERE status 0条件是不是丢了。6.3 答辩切入点的自查清单我自己当年毕业设计做类似项目时栽过一个跟头代码全跑通了但老师问“订单超时关闭是怎么实现的”时我才发现自己只写了一个Thread.sleep的假定时任务。所以后来我做这种项目一定会按下面这个清单自查一遍能否一句话说清订单状态机的每个状态转移条件能否讲清为什么用数据库乐观锁而不是 synchronized能否指出 JWT 的过期时间和 Redis 里的 token 失效策略能否解释前端打包后放进 Spring Boot 为什么必须用 hash 路由能否说明金额字段为什么用 BigDecimal每个问题对应的都是这篇文里讲过的设计决策。能回答上来说明你是真的“设计与实现”了而不是照着网上的代码跑通而已。最后说一个我的个人教训做校园外卖这类带状态流转的系统永远要把状态机画在纸上再写代码。我第二次做的时候直接先画状态转移图两周就完成了整个项目而且基本没返工。这个方向不难难的是别一上来就埋头写代码。希望帮到你。本文还有配套的精品资源点击获取
返回列表