ARTICLE DETAIL

资讯详情

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

SpringBoot实战:高并发食堂订餐系统的库存扣减与订单状态机

SpringBoot实战:高并发食堂订餐系统的库存扣减与订单状态机 做Java毕业设计选什么题目是每年都有一批同学纠结的事。我这个过来人给一个思路把目光投向校园里每天都在发生的高频场景——食堂。一到饭点窗口前排起长队阿姨一边打菜一边算账后面的人伸着脖子看今天有什么菜刷卡机偶尔还会卡壳。这种体验我相信每个人都经历过而它背后正好藏着一个完整、有实际价值、又能把SpringBoot全家桶串起来的项目。我的毕业设计题目就定为“米果智能食堂管理系统”本质上是用Java和SpringBoot搭建一个Web版的校园食堂线上订餐平台让用户可以在网页上浏览菜品、加购物车、下单支付食堂端接单出餐管理端查看营业数据。这篇文章把我从需求分析、库表设计、核心代码实现到踩坑debug的整个过程写出来希望能给正在做同类题目或者想用SpringBoot多模块练手的同学一些可以直接借鉴的经验。很多同学会问食堂订餐系统网上不是有一堆现成的吗确实有但把需求做透、把并发和库存问题真正处理好就是另一回事了。下面我按项目的实际推进顺序从选型的底层逻辑说到具体实现细节。1. 为什么“智能食堂点餐”适合作为Java毕设题目或者说它到底解决了什么问题先别急着写代码把题目背后的需求看明白后面设计表结构、划分模块时思路会顺很多。1.1 真实场景里的痛点就是系统要解决的需求校园食堂的几个典型痛点我总结下来是这几条排队时间长下课时间集中窗口数量有限点餐、结算、取餐全在窗口完成效率被卡在“人”身上。信息不透明学生走到窗口才知道今天有什么菜价格也是到跟前才看没法提前安排。错峰能力弱所有人都挤在中午12点食堂容量被顶到极限但11点和13点又比较空。统计靠人工哪个菜卖得好、每天营业额多少、原材料需要备多少基本靠食堂管理员拍脑袋。智能食堂点餐平台本质上就是把“点餐”这个动作从窗口前移到网页上学生提前浏览菜品、加购物车、提交订单食堂端根据订单统一备餐用户按照约定时间到窗口取餐。这样既压缩了现场决策时间也能让食堂掌握更准确的备餐数据。放在毕设语境里这个需求足够真实又不至于复杂到一个人做不完是一个很恰当的课题边界。1.2 这个题目的价值从“毕设评估”的三个角度来打分我当年评估题目时是从三个维度看的第一技术覆盖面。这个题目能自然地把SpringBoot、MyBatis-Plus、MySQL、Redis、前端Vue/HTML模板、文件上传、定时任务、接口鉴权串在一起技术栈主流面广但都有成熟方案不会做到一半卡死。第二业务闭环完整。从用户注册登录到菜品浏览、加购物车、提交订单、支付模拟、食堂接单、出餐、评价再到管理端的营业统计整条路径是通的。毕设答辩时老师最看重的是“你做的系统是一个能跑通的完整业务闭环”而不是一个只写了一半的Demo。第三有可深挖的难点。一个看起来平平无奇的订餐系统往里挖会发现并发扣库存、超时未支付订单释放、订单状态机等问题。这些点恰恰是答辩时用来证明“这个项目确实是我想清楚了”的关键素材也是实际开发中最容易踩坑的地方。1.3 系统角色的划分决定了功能清单我把使用角色拆成三类每个角色对应一套功能清单。这样的划分直接决定了后面前端页面和后台接口的组织方式。角色核心功能说明普通用户学生/教职工注册登录、浏览菜品、加入购物车、提交订单、模拟支付、查看订单状态、催单、评价反馈系统的前端主流程占了80%的页面量食堂管理员菜品管理、库存管理、分类管理、接单出餐、查看当日订单、营业数据统计后台业务的核心订单状态流转主要在这个端系统管理员用户管理、食堂信息管理、数据大屏展示、基础参数配置偏运维和全局管理页面不多但必须有这里有个容易被忽略的细节大多数毕设项目只做了“用户下单”这一半食堂端往往只是对订单做个列表展示。我建议把“接单出餐”这个环节做成真正的状态流转——食堂管理员可以点击“开始制作”“出餐完成”这样订单状态就不再是死数据而是有生命周期的业务对象答辩时可以说清楚你设计的状态机。2. 技术栈选型与项目整体架构为什么是SpringBoot而不是更“老”的方案选题定了以后第二步是确定技术方案。这一点我的建议很明确只要不是学校强制要求直接上SpringBoot。2.1 SpringBoot解决的是“配置地狱”让你把时间花在业务上很多教材还在教SSM框架SpringMVC Spring MyBatis的XML配置十几行的bean定义、数据源配置、事务配置手动拼得眼花缭乱。SpringBoot把这些约定成俗的东西用自动配置吃掉一个spring-boot-starter-web加进来内嵌Tomcatmain方法一启动Web服务就起来了。我做这个项目时最大的感受是SpringBoot不是帮你省掉了学习底层的步骤而是让你在毕设周期内把有限的时间花在业务逻辑、数据库设计这些更有价值的事情上。当然这不意味着可以完全不懂原理。比如SpringBootApplication为什么一个注解就能开启自动配置EnableAutoConfiguration加载了什么这些是答辩时大概率会被问到的还是得能讲明白。2.2 分层架构Controller-Service-Mapper怎么分工项目采用前后端分离的开发方式。后端是标准的SpringBoot三层架构Controller层负责接收HTTP请求、参数校验、返回统一响应结果不写任何业务逻辑。Service层业务逻辑的核心包括事务控制、库存扣减、订单状态流转等。Mapper层DAO层用MyBatis-Plus封装好的BaseMapper完成数据库操作复杂SQL再单独写。我用的技术组合是这样的组件选型理由基础框架SpringBoot 2.7.x稳定、教程多、兼容MySQL驱动方便ORM框架MyBatis-Plus单表CRUD不用写XML自带分页插件和代码生成器数据库MySQL 8.0社区版免费、生态成熟、教务环境基本都装了缓存/分布式锁Redis缓存菜品热度、分布式锁防止库存超卖前端框架Vue 3 Element-Plus组件化开发快Element-Plus的表格和表单能省一半工作量接口文档knife4j (OpenAPI3)自动生成接口文档自测和答辩演示都方便2.3 项目目录结构直接照着搭就行一个清晰的目录结构是后期不混乱的前提我建议按“模块分包”而不是“按功能堆类”来组织。下面是我的目录结构可以参考migu-canteen/ ├── src/main/java/com/example/migu/ │ ├── MiguApplication.java # 启动类 │ ├── common/ # 通用模块 │ │ ├── Result.java # 统一响应封装 │ │ ├── PageResult.java # 分页响应封装 │ │ └── GlobalExceptionHandler.java # 全局异常拦截 │ ├── config/ # 配置类MybatisPlus、Redis、CORS、定时任务 │ ├── controller/ # 控制层 │ │ ├── UserController.java │ │ ├── DishController.java │ │ └── OrderController.java │ ├── service/ # 业务层 │ │ ├── DishService.java │ │ └── impl/ │ ├── mapper/ # 数据访问层 │ ├── entity/ # 数据库实体 │ ├── dto/ # 请求/响应对象 │ └── utils/ # 工具类JWT、Redis操作 └── src/main/resources/ ├── application.yml └── mapper/ # 复杂SQL的XML文件这个结构的关键点在于entity只对应数据库表结构dto专门用来和前端交互。很多同学刚开始喜欢直接返回Entity给前端结果密码、状态码这些字段全暴露了迟早要返工。从第一天就养成Entity和DTO分离的习惯后面省心得多。3. 数据库设计订单、菜品、库存之间的“联动关系”是核心数据库设计是整个项目的地基。我第一版是照着“有哪些页面”来建表的结果做到订单模块时发现缺东少西又回头改表结构白白浪费了两天。正确的做法是先梳理业务流程再根据流程里的每个动作设计表。3.1 核心业务链路整个点餐业务的核心链路是用户浏览菜品 → 加入购物车 → 提交订单生成订单主表和明细表 → 模拟支付修改订单状态 → 食堂接单 → 制作出餐 → 完成订单 → 用户评价从这个链路反推核心表至少要有这些表名核心字段作用userid, username, password, nickname, avatar, phone, role存放用户和管理员信息用role字段区分角色canteenid, name, address, open_time, close_time食堂基本信息为将来扩展多食堂做准备categoryid, name, sort, canteen_id菜品分类比如套餐、盖饭、饮品dishid, name, image, price, category_id, status, sales, stock菜品基本信息冗余一个stock字段表示库存cart_itemid, user_id, dish_id, quantity, status购物车条目order_infoid, order_no, user_id, canteen_id, total_amount, status, remark, create_time, pay_time, finish_time订单主表记录一笔订单的整体状态order_itemid, order_id, dish_id, dish_name, dish_image, price, quantity订单明细表记录下单时每道菜的信息commentid, user_id, order_id, content, rating, create_time用户对菜品或订单的评价3.2 两个关键设计决策为什么要冗余字段建表时有三个点我当时犹豫过最后踩坑踩出的结论第一订单明细表里为什么重复存dish_name和dish_image因为菜品的价格和名称是会变的——搞一次打折活动或者食堂改了菜名如果你在订单明细里不冗余存储历史订单显示的价格就跟着变了这不符合业务直觉。用户看到的是“下单时的价格”不应该是“现在的价格”。这就是快照思想在电商、外卖系统里都是这么做的。第二为什么dish表里既要有price又要有stockprice是菜品价值属性stock是库存约束属性。库存的存在是为了让“售罄”这件事成为系统的硬约束而不是靠前端按钮置灰来假装有约束。第三主键用什么策略我的建议是订单表使用“时间戳随机数”生成业务订单号比如202406121530002345678而不是直接用雪花ID或者自增ID。因为订单号要展示给用户自增主键会暴露平台单量纯雪花ID又太长。其他表正常用AUTO_INCREMENT或雪花ID都可以。3.3 库存扣减、释放与售罄判断的SQL思路库存处理是这类系统最值得写进论文、也最值得在答辩时展开的技术点。我先给出最简单正确的SQL写法。下单时扣减库存的正确姿势不是先查库存再UPDATE而是一句带条件的UPDATEUPDATE dish SET stock stock - #{quantity} WHERE id #{dishId} AND stock #{quantity}这条SQL的作用是一步完成“判断库存是否够 库存扣减”返回影响行数。影响行数为1说明扣减成功为0说明库存不足或者菜品下架直接抛业务异常“该菜品库存不足”。原理是MySQL行锁在UPDATE时对目标行加锁配合stock #{quantity}条件从根上杜绝了并发下超卖的可能。用户取消订单或者超时未支付时要释放库存UPDATE dish SET stock stock #{quantity} WHERE id #{dishId}不要小看这个加回库存的动作它必须和订单状态变更在同一个数据库事务里执行否则会出现“订单取消了但库存没加回来”的数据不一致。这个点是我在联调时真实遇到过的Bug后面会细讲。4. SpringBoot核心业务实现从用户提交订单到食堂出餐的全流程数据库结构想清楚后剩下的就是按照业务链路把接口一个个填进去。这一节我会把订单模块和库存联动这块的实现逻辑完整展开这也是答辩时最能展示“你确实自己动手写过”的部分。4.1 订单状态机一张状态流转图就能让老师明白你不是在堆CRUD订单状态不能设计成“下单/完成/取消”三个状态就草草了事那样无法体现食堂接单出餐的中间过程。我设计了六个状态状态码状态名触发动作0待支付用户提交订单后1已支付待接单用户完成模拟支付后2制作中食堂管理员点击“开始制作”3已出餐待取餐食堂管理员点击“出餐完成”4已完成用户点击“确认取餐”或系统自动完成5已取消用户取消/超时未支付自动取消这里有个设计细节状态流转只能单向推进写Service时要对“当前状态是否允许执行这个动作”做校验。比如一笔待支付订单不应该被点击“开始制作”否则食堂端和用户端看到的信息就对不上了。4.2 创建订单的核心逻辑事务、锁与库存扣减的配合创建订单是系统里最复杂的业务方法同时涉及校验菜品、计算总价、扣减库存、生成订单主表、生成订单明细。我贴一个简化版的Service核心代码Override Transactional(rollbackFor Exception.class) public OrderInfo createOrder(OrderCreateDTO dto) { // 1. 校验地址和购物车数据获取购物车条目列表 ListCartItem cartItems cartItemMapper.selectList( new LambdaQueryWrapperCartItem() .eq(CartItem::getUserId, dto.getUserId()) .eq(CartItem::getStatus, 1)); if (CollectionUtils.isEmpty(cartItems)) { throw new BizException(购物车为空无法下单); } // 2. 计算总价并检查每个菜品是否在售 BigDecimal totalAmount new BigDecimal(0); ListOrderItem orderItems new ArrayList(); for (CartItem cartItem : cartItems) { Dish dish dishMapper.selectById(cartItem.getDishId()); if (dish null || dish.getStatus() ! 1) { throw new BizException(菜品[ cartItem.getDishName() ]已下架); } // 扣减库存UPDATE dish SET stock stock - ? WHERE id ? AND stock ? int rows dishMapper.deductStock(dish.getId(), cartItem.getQuantity()); if (rows 0) { throw new BizException(菜品[ dish.getDishName() ]库存不足); } totalAmount totalAmount.add(dish.getPrice().multiply( new BigDecimal(cartItem.getQuantity()))); // 组装订单明细快照 OrderItem item new OrderItem(); item.setDishId(dish.getId()); item.setDishName(dish.getName()); item.setDishImage(dish.getImage()); item.setPrice(dish.getPrice()); item.setQuantity(cartItem.getQuantity()); orderItems.add(item); } // 3. 生成订单主表 OrderInfo order new OrderInfo(); order.setOrderNo(OrderNoGenerator.generate()); order.setUserId(dto.getUserId()); order.setCanteenId(dto.getCanteenId()); order.setTotalAmount(totalAmount); order.setStatus(0); orderInfoMapper.insert(order); // 4. 保存订单明细 for (OrderItem item : orderItems) { item.setOrderId(order.getId()); orderItemMapper.insert(item); } // 5. 清空购物车 cartItemMapper.delete(new LambdaQueryWrapperCartItem() .eq(CartItem::getUserId, dto.getUserId())); return order; }这里最核心的是Transactional(rollbackFor Exception.class)。为什么必须加事务因为扣库存、插入订单主表、插入订单明细、清空购物车这四个操作必须要么全部成功、要么全部回滚。如果扣了库存但订单生成失败库存就会凭空减少如果订单生成了但库存没扣就会超卖。事务就是给这四个操作上了“同一根绳”。4.3 并发场景下如何防止超卖分布式锁和数据库条件更新如果只有单机单库UPDATE ... WHERE stock ?这个条件更新其实已经能防住超卖了因为MySQL的行锁会让并发的扣减操作串行执行。那为什么还要用Redis分布式锁因为我们的项目里不仅有“扣库存”还有“同一用户同时下单防重复提交”“食堂管理员并发接单”等场景。Redis分布式锁可以把“判断购物车非空→计算总价→扣库存→生成订单”这段关键路径在分布式环境下串行化进一步降低由于业务编排导致的并发bug概率。我用的Redis锁是围绕StringRedisTemplate封装的简单工具类public boolean tryLock(String key, String value, long timeout, TimeUnit unit) { Boolean flag stringRedisTemplate.opsForValue() .setIfAbsent(key, value, timeout, unit); return Boolean.TRUE.equals(flag); } public void unlock(String key, String value) { String currentValue stringRedisTemplate.opsForValue().get(key); if (value.equals(currentValue)) { stringRedisTemplate.delete(key); } }使用方式是在创建订单的方法上加锁String lockKey lock:order:create: userId; String lockValue UUID.randomUUID().toString(); boolean locked redisLockUtil.tryLock(lockKey, lockValue, 5, TimeUnit.SECONDS); if (!locked) { throw new BizException(您正在提交订单请勿重复操作); } try { // 创建订单的主流程代码 } finally { redisLockUtil.unlock(lockKey, lockValue); }这个设计虽然简单但足够在毕设场景下撑住并发演示。答辩时有老师问“你用什么解决并发超卖”你可以从两个层面回答数据库层用条件更新做兜底应用层用Redis锁防止重复提交和业务串行化。4.4 模拟支付与超时未支付订单处理真实对接支付宝/微信支付在毕设里不是必须的但完全不涉及支付业务链条又会断在“提交订单”这一步。我的做法是做一个模拟支付接口前端点击“确认支付”时后端模拟延时200毫秒然后把订单状态从0改为1并记录支付时间。这个接口前端的交互是完整真实的。还有一个必须处理的场景用户下单后一直不支付怎么办如果不处理这些待支付订单会一直占着库存导致其他用户买不到菜。我引入了定时任务每1分钟扫描一次把创建时间超过15分钟且状态为0的订单自动取消并把库存加回Component public class OrderTimeoutTask { Autowired private OrderInfoMapper orderInfoMapper; Autowired private DishMapper dishMapper; Scheduled(cron 0 * * * * ?) public void releaseTimeoutOrders() { ListOrderInfo timeoutOrders orderInfoMapper.selectList( new LambdaQueryWrapperOrderInfo() .eq(OrderInfo::getStatus, 0) .lt(OrderInfo::getCreateTime, LocalDateTime.now().minusMinutes(15))); for (OrderInfo order : timeoutOrders) { // 必须在事务里执行两张表的变更 orderInfoMapper.updateStatusById(order.getId(), 5); ListOrderItem items orderItemMapper.selectList(...); for (OrderItem item : items) { dishMapper.releaseStock(item.getDishId(), item.getQuantity()); } } } }这里的重点就是我在3.3节提到的取消订单和释放库存必须在一个事务里要么一起成功要么一起失败。4.5 食堂端的接单出餐流转食堂端管理员的界面相对简单但逻辑上有个常见误区很多人把“接单”设计成点一下就把状态直接改成“已完成”。我拆成了两步“开始制作”和“出餐完成”中间状态是“制作中”。为什么要拆因为业务直觉是——食堂不可能瞬间做好一份餐制作是一个过程。拆出中间状态才能支撑用户端展示“您的餐品正在制作中”这种提示也让整个订单状态机更完整。对应的两个接口接单接口校验当前状态为1更新为2。出餐接口校验当前状态为2更新为3。用户端“确认取餐”的动作则是把状态从3更新为4。这样一条业务流下来每个角色都有事做每笔订单的状态变化都有明确的操作者答辩演示时可以一步一步点给老师看。5. 实际开发中踩过的坑以及完整的排查思路每个SpringBoot项目都会遇到类似的坑我这里把印象最深的几个记录下来每一个我都给出了排查链路而不是直接甩结论。5.1 超卖问题不是“用不用锁”的问题而是“什么时候加锁”的问题我第一版写创建订单时库存扣减用的是“先SELECT stock再判断再UPDATE”的老三段写法Dish dish dishMapper.selectById(dishId); if (dish.getStock() quantity) { dishMapper.updateStock(dishId, dish.getStock() - quantity); }自测时单用户下单完全没问题直到我用JMeter模拟50个并发用户同时抢购同一道限量菜品发现数据库里的库存变成了负数。排查链路是这样走的复现用JMeter压测创建订单接口50线程并发循环5次监控dish表的stock字段。定位SQL日志发现大量的selectById和updateById交错执行多个线程读到相同的库存值比如20。找原因SELECT和UPDATE是两个独立操作线程A和线程B同时查到20然后A扣减成19B也扣减成19但真实库存应该只剩18。这就是经典的“读改写”竞态条件。修复把“查库存扣库存”合并成一条SQLUPDATE dish SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity}并检查受影响行数。验证重新压测库存不再为负超卖解决。这个坑的教训是并发问题一定要用并发的手段去验证用单线程自测是测不出来的。5.2 事务失效同一个类里调用自己的方法Transactional不管用还有一次我在调试一个用户下单后“扣减积分”功能时发现积分扣了但订单回滚后买的东西居然还在。后来在日志里看到异常信息是Transaction rolled back, but something went wrong才反应过来事务没生效。排查链路加日志确认异常后看数据发现订单没了但库存扣了说明事务没有覆盖到“扣库存”这个动作。看代码结构发现问题出在OrderServiceImpl里的createOrder()方法它调用了一个同类里的私有方法deductStockAndGenerateItems()这个方法加了Transactional。查Spring AOP原理Spring的声明式事务是基于代理实现的。外部调用createOrder()时经过的是Spring生成的代理对象事务能正常拦截但类内部this.deductStockAndGenerateItems()这种调用走的是this原对象没有经过代理注解自然不生效。修复把涉及多表变更的方法拆到另一个Service类中由Spring注入代理对象再调用或者把Transactional统一加在对外入口方法createOrder()上让整个业务方法共享同一个事务。这个坑在答辩时几乎必问建议每个做SpringBoot项目的同学都去把自己的Controller、Service理一遍看看有没有“自调用”导致事务失效的情况。5.3 前端联调时最典型的跨域问题前后端分离后前端跑在http://localhost:5173后端跑在http://localhost:8080浏览器直接请求会被同源策略拦截。我一开始在Controller上加了CrossOrigin单个接口能通但写了一大堆注解实在难看。后来统一用了全局配置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(*)才能兼顾。这个小坑不知道坑了多少人写在这里给大家提个醒。5.4 上传图片后无法访问的误区菜品图片上传很多同学第一反应是传到项目根目录下的static文件夹。我试过结果发现重新打包部署后图片全部丢失因为static在jar包里面图片明明写进去了却没权限访问。正确的做法是在服务器或本机指定一个独立目录存放上传文件比如/data/upload/然后通过配置类映射静态资源路径到本地磁盘Configuration public class WebMvcConfig implements WebMvcConfigurer { Value(${file.upload-dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadDir /); } }浏览器访问http://localhost:8080/upload/20240612/xxx.jpg就能正常显示图片。application.yml里配置file.upload-dir: /data/upload部署时只需确认这个目录存在和代码jar包分离后续升级发布不受影响。6. 答辩时的加分项、常见追问以及毕设做完之后的扩展方向项目代码写完不代表事情结束答辩才是把工作量“讲”出来的关键一环。6.1 老师大概率会问的四个问题提前准备好老师常问问题参考答案思路为什么订单要在明细表里冗余菜品名和价格避免菜品信息变更后历史订单显示错乱保证订单是下单时的快照并发下如何防止重复下单和库存超卖数据库条件更新UPDATE ... WHERE stock ?保证原子性Redis锁防止用户重复提交事务保证扣库存和生成订单的一致性用户取消订单后库存什么时候加回来取消或超时未支付时在同一个事务里改订单状态并执行库存释放SQL系统后期要支持多食堂怎么改现有的表已经设计了canteen_id字段菜品、订单都挂食堂维度只需要新增食堂管理页面和权限隔离即可6.2 建议做但很多同学忽略的“软件工程”加分项除了功能跑通有几个动作能让项目质量上一个档次用knife4j生成接口文档答辩时演示API调试页面比直接贴代码直观得多。写一份《需求分析说明书》和《数据库设计说明书》论文直接有素材。把JMeter压测结果、Redis锁的验证过程截图存档说明你有用工程手段验证过问题。6.3 毕业设计之外的延伸它可以从课程项目变成简历项目做完这个系统之后我最大的感受是它不是一个写完就丢的作业而是能继续长出功能的东西。如果你想让它更有竞争力可以考虑下面几个方向把模拟支付替换成接入支付宝沙箱支付体验真实签名与回调流程。引入消息队列比如RocketMQ处理订单超时和食堂出餐通知实现削峰填谷。加一个基于WebSocket的实时食堂窗口排队叫号功能进一步体现技术的实时交互能力。用Spring Security或Sa-Token做更细粒度的权限控制比如食堂管理员只能操作自己食堂的数据。根据我个人的经验做这类系统有一个通用心法每一次“有一个吸引人的业务点”出现都要逼自己想一想底层逻辑是什么。比如食堂排队的问题表面是排队本质是资源调度订单状态的设计表面是字段本质是业务状态机。把这些想明白了不仅仅是毕设能拿高分以后在真实工作中做企业级项目时遇到订单系统、餐饮食堂系统、库存系统你都会发现很多原则是相通的。最后再分享一个小技巧整个项目开发过程中要养成每改一个Bug就写一段Bug记录的习惯写清楚现象、排查步骤、根因、解决方案。我的毕业论文里的“系统测试与调试”一章几乎就是靠这份记录撑起来的不用临时抱佛脚。真的做毕设最怕的不是代码有Bug而是说不出Bug背后的原因。你有了这些底料答辩的时候就踏实了。
返回列表