ARTICLE DETAIL

资讯详情

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

Spring Boot校园点餐平台实战:从架构设计到核心代码全解析

Spring Boot校园点餐平台实战:从架构设计到核心代码全解析 这次带的这个项目是典型的 Java 课程设计/毕业设计级别的完整工程——基于 Spring Boot 的校园点餐平台。不是那种只贴几个 Controller 片段的水货而是从用户登录、菜品浏览、购物车、下单支付到商家接单、订单管理的全流程闭环源码、SQL 脚本、说明文档一套齐全。我拿到手之后先跑通了完整链路又顺手拆了一遍核心模块发现这套代码的工程结构比很多培训班项目要清爽不少适合做毕设、课程设计也适合刚学完 Spring Boot 想找一个“能写进简历里的完整项目”来练手的同学。这篇文章就基于这套源码把整个平台的模块设计、技术选型、核心代码逻辑和我在复现过程中踩过的坑一条条讲透。1. 校园点餐平台的需求拆解与整体设计1.1 这个平台到底解决了什么问题校园食堂的痛点其实很典型饭点排队时间长窗口菜品信息不透明学生们不知道哪个窗口现在人少、今天有哪些菜只能挤到现场去碰运气。校园点餐平台要解决的就是这三点——提前点餐、错峰取餐、信息公开。这个项目的核心角色分三类学生普通用户、商家食堂窗口、管理员平台运营方。围绕这三个角色功能边界其实很容易画出来。学生端注册登录、浏览菜品分类、搜索菜品、查看菜品详情、加入购物车、提交订单、在线模拟支付、查看个人订单列表、取消未接单的订单。商家端管理自己的菜品上下架、改价格、改库存、查看顾客下的新订单、接单/完成订单。管理员端管理所有用户禁用/启用商家账号、审核商家入驻、管理菜品分类、查看平台整体订单数据。从这套权限模型能看出来它比常见的“单用户随便点两下”的示例项目完整不少至少把角色权限分开了而不是所有人共用一张表一个接口。这在校级项目里算是一个很不错的加分项。1.2 数据库设计七张表撑起整个业务骨架接手源码的第一件事永远是看数据库脚本。这套项目用的表结构并不复杂属于“麻雀虽小五脏俱全”的类型核心就七张表。表名作用关键字段user用户表区分学生/商家/管理员id, username, password, role, statuscategory菜品分类表比如川菜、快餐、饮品id, name, sortdish菜品表属于某个分类归属于某个商家id, name, category_id, price, image, stock, merchant_idcart购物车表id, user_id, dish_id, quantityorders订单主表id, order_no, user_id, merchant_id, total_price, status, create_timeorder_item订单明细表id, order_id, dish_id, dish_name, price, quantityaddress配送/取餐地址表可选扩展id, user_id, detail设计上值得注意的两个点第一订单和订单明细拆开成主从两张表这是电商订单模块的标配做法因为一个订单可能包含多个菜品主表存总价和状态明细表存每一项的快照信息第二菜品表里有 merchant_id 字段把菜品归属到了具体商家下单时按商家维度聚合生成订单方便商家在后台只看到属于自己的订单。1.3 接口设计思路RESTful 风格是底线看完整套 Controller能感受到作者是按 RESTful 的规范去写的。资源用名词复数操作借用 HTTP 方法语义GET 表示查询、POST 表示新增/提交、PUT 表示更新、DELETE 表示删除。比如POST /api/user/register 注册POST /api/user/login 登录GET /api/dish/list 获取菜品列表POST /api/cart/add 加入购物车POST /api/order/submit 提交订单GET /api/order/my 查询我的订单PUT /api/order/cancel 取消订单接口路径虽然没有做得极度细致但整体语义清晰状态码也沿用了 HTTP 标准。对于教学项目而言这种规范程度已经完全够用而且面试时被问到“你项目里的接口怎么设计的”你完全可以照着这套 RESTful 思路讲出逻辑来。2. 技术栈选型与核心配置解析2.1 为什么选 Spring Boot MyBatis-Plus MySQL这套技术栈可以说是目前国内 Java 后端项目最主流的组合没有之一。Spring Boot 负责快速搭建和自动配置MyBatis-Plus 负责简化数据库操作MySQL 负责数据落地存储。Spring Boot 的核心价值在于“约定大于配置”。你不需要像传统 SSM 项目那样写一堆 XML 配置文件只需要在 application.yml 里声明数据源、端口、JWT 密钥等关键参数框架会自动完成绝大多数 Bean 的注册。对于这种中小型单体项目来说开发效率提升非常明显。MyBatis-Plus 则是 MyBatis 的增强工具。它最大的贡献是内置了通用 Mapper单表 CRUD 连 SQL 都不用写直接调用 BaseMapper 提供的方法就行。比如你定义一个 DishMapper 继承 BaseMapperDish那么 insert、deleteById、selectById、updateById 这些方法就全都自动有了。本项目实际的业务表也就那几张用 MyBatis-Plus 会把样板代码压缩一大截。至于 MySQL8.x 版本在性能、窗口函数、JSON 支持上都比 5.7 强不少。这里有个细节要注意MySQL 8.0 的驱动类名改成了 com.mysql.cj.jdbc.Driver而且 URL 里必须加上 serverTimezone 参数不然会因为时区问题报错。这个后面会在避坑部分展开讲。2.2 Spring Boot 版本选择的学问2.x 还是 3.x拿到源码后第一件事就是看 pom.xml 里的 Spring Boot 版本。这套项目用的是 2.7.x 系列这是个非常稳妥的选择。为什么这么说现在 Spring Boot 3.x 虽然已经全面普及但它有几个硬性门槛最低要求 JDK 17、jakarta 命名空间迁移、旧版第三方依赖兼容性不确定。对这些做毕设或课程设计的同学来说如果你的电脑装的是 JDK 8直接上 Spring Boot 3.x 那是必崩无疑。而 2.7.x 是 Spring Boot 2.x 的最后一个主干版本既有完善的生态支持也兼容 JDK 8恰好是教学项目的最佳落点。顺带说一个很经典的坑如果给你源码的人用的是 Spring Boot 2.4 之前那么 MultipartResolver 需要显式声明而 2.4 之后的 Spring MVC 默认就不需要了。这套项目里没有写相关的配置说明版本确实比较新所以它是符合默认行为的。2.3 配置文件和依赖清单速览pom.xml 里的依赖清单非常经典我列一下核心部分parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies !-- Web 支持 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus 启动器 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version scoperuntime/scope /dependency !-- Lombok 简化实体类 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- JWT 工具 -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency /dependencies这里有一个升级点值得说jjwt 0.9.1 版本比较老因为它强依赖 JAXB 模块在 JDK 11 上会报 java.lang.ClassNotFoundException: javax.xml.bind.DatatypeConverter。如果你用 JDK 8 跑问题不大但用 JDK 11 就需要引入 javax.xml.bind:jaxb-api 依赖或者直接换成 jjwt 0.11.5 版本、改用新的 API 写法。这个我实测踩过后面细说。再看 application.yml。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/campus_order?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto jwt: secret: your-secret-key expire: 7200mybatis-plus 的 map-underscore-to-camel-case 必须设置为 true这样数据库里的 create_time 字段才能自动映射到 Java 实体类的 createTime 属性否则查询结果里会出现一堆 null 值。字段自动驼峰映射是 MyBatis 的老传统很多人第一次用 MyBatis 查出来的时间字段为 null八成都是这个配置没开。jackson 的 date-format 保证后端返回 JSON 时日期格式统一避免前端拿到一串时间戳还要自己去格式化。这些都是细节问题但面试时能说出“我配置了全局时间格式化避免前后端日期格式不一致”这句话会比背八股文加分得多。3. 核心功能实现与代码逻辑走读3.1 用户登录鉴权JWT 的无状态方案看到源码里用了 JWT 做登录态管理这个设计值得点个赞。传统用 Session 的做法在单体项目里没问题但一旦做前后端分离Session 跨域要配置一堆 CORS 参数而且后端集群化之后 Session 共享也是个麻烦事。JWT 把用户信息加密生成一个 token前端每次请求带上这个 token后端只需要验签即可天然适合前后端分离架构。核心逻辑分三步第一步登录接口校验用户名密码生成 token。public String login(String username, String password) { User user userMapper.selectOne( new LambdaQueryWrapperUser() .eq(User::getUsername, username) .eq(User::getPassword, MD5Util.encode(password)) ); if (user null) { throw new RuntimeException(用户名或密码错误); } if (user.getStatus() 0) { throw new RuntimeException(账号已被禁用); } // 生成 JWT String token JwtUtil.generateToken(user.getId(), user.getUsername(), user.getRole()); return token; }密码这里一定要提醒一下项目里用的是 MD5 加密这在早年是主流但现在看安全性偏弱因为 MD5 已经被彩虹表全面攻破。做课程设计可以用但如果想把这个项目往生产级靠建议至少换成 BCrypt 或 SHA-256 加盐。这个点你在项目答辩时可以当“已知不足和改进方向”来讲会很加分。第二步写一个拦截器统一校验所有需要登录的请求。public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } token token.substring(7); try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { response.setStatus(401); return false; } } }第三步在 WebMvcConfigurer 里注册这个拦截器并配置放行路径。Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JwtInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns(/api/user/login, /api/user/register); } }注意注册顺序问题。拦截器里的 excludePathPatterns 如果漏配了登录和注册接口前端就永远无法登录——因为登录接口自己也被拦截了返回 401。我在初次跑这个项目时还真犯过一个低级错误结果在浏览器里点了半天登录按钮没有任何反应最后看控制台才发现登录请求被拦了。这种“自己锁死自己”的 bug 调试起来很隐蔽建议一上来就把放行路径检查好。3.2 菜品与购物车MyBatis-Plus 的经典用法菜品查询是 C 端最核心的接口之一。源码里用 LambdaQueryWrapper 做了带条件的分页查询既有分类过滤也有关键字模糊搜索。public PageDish getDishPage(int pageNum, int pageSize, Long categoryId, String keyword) { PageDish page new Page(pageNum, pageSize); LambdaQueryWrapperDish wrapper new LambdaQueryWrapper(); wrapper.eq(categoryId ! null, Dish::getCategoryId, categoryId) .like(StringUtils.hasText(keyword), Dish::getName, keyword) .eq(Dish::getStatus, 1) .orderByDesc(Dish::getId); return dishMapper.selectPage(page, wrapper); }这段代码最值得学的地方是 eq 方法的第一个布尔参数。它实现了“动态条件拼接”——只有满足条件时才会把这一句加进 SQL避免了写一堆 if 判断来手动拼 SQL 的繁琐。这是 MyBatis-Plus 相对原生 MyBatis 非常核心的一个便利点。购物车模块的数据结构很简单就是 user_id dish_id quantity 的组合。加入购物车的时候先查一次库里有没有同款有就数量加一没有就新增一条。这是很典型的“有则更新、无则插入”逻辑。3.3 下单流程一个事务要管住库存和订单下单是整个项目最复杂的业务动作它同时涉及订单主表、订单明细表、菜品表库存扣减、购物车清理四件事。这套源码里用 Transactional 注解把整个事务串起来了Transactional(rollbackFor Exception.class) public Order submitOrder(Long userId, ListCartItem cartItems) { // 1. 创建订单主表 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setStatus(0); // 待支付/待接单 order.setCreateTime(new Date()); orderMapper.insert(order); // 2. 遍历购物车项写入订单明细 BigDecimal total BigDecimal.ZERO; for (CartItem item : cartItems) { Dish dish dishMapper.selectById(item.getDishId()); if (dish null || dish.getStatus() ! 1) { throw new RuntimeException(菜品不存在或已下架: item.getDishId()); } // 检查库存 if (dish.getStock() item.getQuantity()) { throw new RuntimeException(菜品库存不足: dish.getName()); } OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setDishId(dish.getId()); orderItem.setDishName(dish.getName()); orderItem.setPrice(dish.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItemMapper.insert(orderItem); // 3. 扣减库存 dish.setStock(dish.getStock() - item.getQuantity()); dishMapper.updateById(dish); total total.add(dish.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 4. 更新订单总价 order.setTotalPrice(total); orderMapper.updateById(order); // 5. 清空购物车 cartMapper.delete(new LambdaQueryWrapperCart().eq(Cart::getUserId, userId)); return order; }这里有几个很值得注意的细节。第一Transactional 写在实现类上而不是 Controller 上这样事务边界正好覆盖完整的业务操作防止一部分成功一部分失败的场景。第二先查库存再扣库存存在并发超卖风险——两个用户同时看到最后一份菜同时下单最后可能都成功。严格方案要用悲观锁SELECT FOR UPDATE或乐观锁版本号来解决但作为教学项目这里的实现已经能说明事务的思路了你可以在答辩时主动提出来说“我了解到这是并发场景下的超卖问题可以考虑引入乐观锁优化”这会比等着老师来问强得多。订单号生成这段也大有讲究。源码里用了日期时间随机数的拼接方式保证同一秒内多笔订单不撞号。更专业的方案是引入雪花算法但如果你做的项目只跑单机这种拼接方式完全足够。3.4 订单状态流转从待支付到已完成订单状态是这个项目的数据核心状态字段从前到后经历这么几个阶段状态值状态含义说明0待支付/待接单学生提交订单后默认状态1已接单/制作中商家接单后置为此状态2已完成商家出餐订购流程终结3已取消商家未接单前学生可取消4已退款取消后如果已支付走退款流程这个状态机的设计非常像一个简化版的“真实外卖平台”了。学生端只能对状态为 0 的订单发起取消商家端只能把状态为 0 的订单流转到 1再把 1 流转到 2。单向流转不能跳状态——比如不能从待支付直接跳到已完成这在业务上是不合理的。订单接口里有一个很实用的细节查询“我的订单”时源码返回的数据里如果商家还没接单前端页面上会显示一个“取消订单”按钮一旦商家接了单按钮就消失。这个交互背后的逻辑就是前端拿订单状态值做了判断。状态机的边界设计直接影响前端交互很多课程设计项目状态乱跳就是因为后端接口没做状态校验。4. 前端与后端的联调实现4.1 前后端分离架构下的跨域处理校园点餐平台的前端可能是 Vue 项目也可能是这套源码里自带的简单 H5 页面。不管哪种形态前后端分离一定会遇到跨域问题。Spring Boot 后端解决跨域最干净的方式是配置 CorsFilter。源码里是这么处理的Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedHeader(*); config.addAllowedMethod(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }这里有个注意点如果使用了 JWT 的 Authorization 头那么 addAllowedHeader() 必须配置否则浏览器会认为这个自定义头不在允许列表里直接拦截请求。还有如果前端请求携带了 cookie比如 axios 默认 withCredentials: true那么 addAllowedOriginPattern() 和 setAllowCredentials(true) 同时使用时Spring 旧版本会报 “When allowCredentials is true, allowedOrigin cannot be *” 的错误。项目用的是 addAllowedOriginPattern这是 SpringBoot 2.4 之后专门解决“允许来源 携带凭证不能同时用通配符”的问题的。这个点非常细节但面试官极爱问。4.2 前端接口调用封装技巧参考这套项目的接口设计前端通常会把请求封装成统一的 API 模块。比如在 Vue 里import axios from axios const service axios.create({ baseURL: /api, timeout: 10000, headers: { Content-Type: application/json } }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { Message.error(登录已过期请重新登录) router.push(/login) } return Promise.reject(error) } )这段封装代码是这个项目能流畅联调的隐形功臣。拦截器统一注入 token响应拦截器统一处理鉴权失败前端开发根本不用在每个页面里重复写“从 localStorage 拿 token”的逻辑。源码如果前端部分用了类似封装务必要重点读因为它才是前后端能否顺畅配合的关键。4.3 文件上传菜品图片的本地存储方案菜品要传图片这个功能逃不掉。源码里使用的方案是本地文件存储——前端把图片文件 POST 到后端的 /api/upload 接口后端把文件写到项目的 static/upload 目录然后返回一个可访问的 URL。PostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(上传文件不能为空); } String originalFilename file.getOriginalFilename(); String suffix originalFilename ! null ? originalFilename.substring(originalFilename.lastIndexOf(.)) : .jpg; String newFileName System.currentTimeMillis() _ UUID.randomUUID() suffix; String savePath uploadDir File.separator newFileName; file.transferTo(new File(savePath)); // 返回访问路径 String url /upload/ newFileName; return Result.ok(url); }文件名用“时间戳 UUID 原后缀”重命名一是避免中文名字符集问题二是防止不同用户上传同名文件互相覆盖。这是一个典型的防重名方案虽然简单但很实用。不过本地存储方案有一个硬伤如果把代码部署到云服务器重启时上传目录下的文件可能会被构建流程清掉而且项目有多实例部署时用户上传到 A 服务的文件在 B 服务上访问不到。生产级方案一般是接对象存储如阿里云 OSS 或自建 MinIO。有趣的是热词里正好出现了“minio 加入到 springboot”这个方向可以作为项目的进阶改造方案在答辩时提出来会显得你思考过生产环境的差异。4.4 部署构建从源码 jar 包到可运行项目拿到项目以后最常被问的问题是“怎么把它跑起来”。我给出一份稳妥的操作顺序先把 MySQL 建库执行 sql 目录下的初始化脚本然后在 IDEA 里打开项目等待 Maven 依赖下载完成修改 application.yml 里的数据库账号密码如果有 Redis 相关配置也要确认本机 Redis 已启动直接运行主启动类。如果是打包部署用 Maven 的 package 命令打成 jar 包然后 java -jar 启动。mvn clean package -DskipTests java -jar target/campus-order-0.0.1-SNAPSHOT.jar打包时最容易出的问题就是测试类不过或资源文件没打进去。DskipTests 是跳过测试的常用参数资源文件问题则需要检查 pom.xml 里有没有显式配置 resources。Spring Boot 默认会把 src/main/resources 下的文件打进 jar 包一般不会出问题但如果有人改了 pom 的 build 配置就有可能把配置文件漏掉启动时直接报找不到数据源。5. 常见问题排查与避坑实录5.1 环境层面的四座大山跑这套代码时最容易失败的不是业务逻辑而是环境适配。我把高频问题整理成了一张速查表全部是我实跑验证过的现象根本原因解决方式启动时报 ClassNotFoundException: javax.xml.bind.DatatypeConverterJDK 11 环境下运行 jjwt 0.9.1引入 jaxb-api 依赖或升级 jjwt 到 0.11.x数据库连接报 Access denied for user密码错误或用户权限不足检查配置文件的 username/password授权远程访问用 GRANT ALL数据写入中文乱码MySQL 连接串没加 characterEncodingutf8或表本身是 latin1修改 url 加 characterEncodingutf8ALTER TABLE CONVERT TO CHARACTER SET utf8mb4查询出来的时间比实际晚 8 小时MySQL 连接串缺少 serverTimezone或 Jackson time-zone 配置不对url 加 serverTimezoneAsia/Shanghai同时设置 Jackson time-zone这四项配置是“跑通项目的第一道关卡”任何一项错了后面全白搭。尤其是时间问题初学者很容易忽略开发时本地一切正常、部署到服务器就晚 8 个小时其实就是时区配置不一致。5.2 业务逻辑里的三个隐蔽坑除了环境问题业务层面也有几个隐蔽故障点值得分享。第一个是 MyBatis-Plus 分页不生效。很多人加了 PageHelper 依赖或用 selectPage 方法但查出来的数据还是全量这是因为 MyBatis-Plus 3.4.0 之后分页必须显式配置 PaginationInnerInterceptor否则分页插件不会自动注册。源码里如果在 MybatisPlusConfig 中加了 Bean 配置分页拦截器那没问题如果没加分页就会静默失效。检查办法控制台打印的 SQL 里有没有 LIMIT 语句。第二个是 JWT 过期时间设置过短。源码配置里 expire 是 7200 秒也就是 2 小时。如果你们演示时中途休息了一会儿回来再点页面就会 401。这不是 bug而是过期机制在起作用但演示时很尴尬。我一般建议演示环境把过期时间调大一些而在正式环境保留合理的过期时间并完善“重新登录”跳转。第三个是 Lombok 的 Data 在不同实体类之间形成循环引用。比如 Order 实体里包含了 User 实体而 User 实体里又包含了 ListOrder序列化时 Jackson 会无限递归导致栈溢出。如果项目里遇到 StackOverflowError八成就这个问题。解决方式是在字段上使用 JsonIgnoreProperties 或 JsonIgnore 打断循环。5.3 从课程设计到生产级还有哪些路要走这套源码作为课程设计绰绰有余但如果真想拿它当跳板往简历里写一个“还算完整”的项目我建议往这几个方向做扩展第一接入 Redis 做热点数据缓存。菜品列表和高频访问的分类信息是典型的读多写少数据用 Redis 做缓存可以大幅降低数据库查询压力。Spring Boot 整合 Redis 只需要引入 spring-boot-starter-data-redis再写一个缓存工具类核心改造量并不大。第二库存扣减升级乐观锁。在菜品表加一个 version 字段更新时用 update ... set stock stock - #{num}, version version 1 where id #{id} and version #{version}通过更新行数判断是否并发冲突能把超卖问题直接堵住。第三订单超时自动取消机制。还没支付的订单不能一直占着库存。最简单的方案是定时任务扫描超过 30 分钟未支付的订单并自动取消也可以用 RabbitMQ 延迟消息更优雅地实现。第四做移动端或者小程序适配。校园场景天然适合小程序如果能把现有的 Vue 前端套到小程序框架上或者单独开发一个小程序端项目的使用场景完整度会上一个台阶。这些扩展不是空话每一条都有明确的技术指向对你理解 Spring Boot 生态非常有帮助而且面试时这些都是加分点。我个人在实际操作中最深的体会是这类“附源码”项目最快的学习方式不是从头到尾把人家的代码看一遍而是先跑通再故意拆坏然后自己去修复。把菜品接口报文格式改错、把库存扣减的并发问题复现一遍、把 JWT 密钥换掉看看会不会全局失效——这些“破坏性实验”做完一遍你对这套系统的理解深度会远超只看源码的人。这套校园点餐平台是一个非常适合练手的完整案例建议你拿到手之后先跑一遍再按上面说的扩展方向改一版收获一定比想象中大。
返回列表