每次到毕业设计选题季总有学弟学妹来问我“仓库管理系统是不是太简单了会不会被老师打回来”说实话这个题目确实经典到有点“烂大街”但正因为它经典反而最能考察一个学生的基本功。我从带毕设和做企业级项目的经验来看仓库管理系统WMS的核心不在“增删改查”本身而在于库存数据的准确性、业务流程的完整性以及并发场景下的数据一致性——这些恰恰是企业面试时最常问、也是很多同学最容易翻车的地方。这篇博文就围绕“基于Java的仓库管理系统的设计与实现”这个项目把从需求拆解、技术选型到数据库设计、核心代码落地、部署排坑的完整链路讲清楚适合正在做毕设的同学也适合想转行Java开发、需要一个拿得出手的项目经验的初学者参考。我先把话放这儿这套系统如果你只是照着别人的代码敲一遍最多拿个合格分但如果你把它当成一个小型进销存产品来做认真思考每一个表字段为什么这么设计、每一次库存变更为什么必须走事务那它完全可以成为你简历上最能打的Java项目甚至还能在面试时帮你顶住三轮技术拷问。1. 核心需求拆解仓库管理系统到底在管什么很多同学拿到题目就急着写代码结果做完发现功能是有了但逻辑根本经不起推敲库存怎么算都对不上、出库之后库存变成负数、权限形同虚设。这些问题的根源通常不是代码写得差而是压根没把“仓库管理系统”的业务边界想清楚。1.1 业务场景决定了功能边界仓库管理系统不是简单的“商品增删改查”。它服务的核心场景是企业仓储流转采购的货到了要入库销售出库要发货仓库内部偶尔要盘点一个仓库放不下或者门店调拨要移库货品有保质期、批次、序列号的要单独追踪。因此一套及格的WMS至少需要包含以下几个模块基础数据用户、角色、菜单权限、供应商、客户、商品分类、商品档案、仓库、货位。入库管理采购入库单的创建、审核、入库操作货品进入库存时明细可追溯。出库管理销售出库单、领料出库单等出库时自动校验库存是否充足。库存管理实时库存查询、库存流水也叫库存变动记录、库存预警低于安全库存提醒、盘点单。调拨管理仓库之间或库位之间的货品转移不能直接改库存必须走调拨单审核流程。报表统计入库汇总、出库汇总、库存台账、进销存报表。这里有个很重要的设计原则所有的库存变动都不能在业务代码里直接“update 库存表”而必须通过单据流水的方式驱动。因为库存是一个结果数据它是由一次次入库、出库动作累加出来的如果今天张三直接在库存表上改数字明天李四再改一下最后账实不符时谁也查不出来。所以业务上一定要保留“流水账”。1.2 功能性需求之外别忽略非功能性需求除了功能我在给同学评审时还会看几个非功能维度的东西并发安全电商大促场景下同一个SKU可能被多个订单同时出库怎么保证不超卖。数据一致性一张出库单要扣减多个商品库存如果循环扣到第三个商品时报错前面两个已经扣了怎么办。操作留痕谁在什么时候做了什么操作必须有日志否则出了问题没法追责。权限控制普通操作员不能审核自己创建的入库单仓库管理员和系统管理员权限必须分开。这些要求单拎出来每一个都比“实现CRUD”难一个量级但恰恰是毕业设计答辩时的加分点和面试时的高频考点。1.3 角色与权限模型的简化设计我见过很多同学直接在代码里写if (user.getRole().equals(admin))这种设计在demo里能跑但稍微复杂一点就崩了。正规项目里用的是RBAC基于角色的访问控制模型用户分配角色角色绑定菜单/权限登录后通过拦截器或框架校验接口权限。我建议毕设系统采用三张核心表用户表、角色表、用户角色关联表再加上菜单表和角色菜单关联表。菜单权限用按钮级别控制比如“审核”按钮只有特定角色可见、可点这样答辩时老师问起来你可以从“横向越权”和“纵向越权”两个角度展开非常加分。2. 技术选型逻辑Spring Boot MyBatis Plus MySQL为什么够用技术选型这件事很多毕设党容易走极端。要么直接用最原始的ServletJDBC写代码量巨大且没有工程化思维要么把微服务、Redis、MQ、Docker全往上堆结果自己根本讲不清楚答辩一问就露馅。仓库管理系统这个体量Spring Boot MyBatis Plus MySQL的组合是性价比最高的方案没有之一。2.1 为什么是Spring Boot而不是Spring MVC如果你还在用Spring MVC加一堆XML配置那几乎等于自动放弃了现代化开发方式。Spring Boot的核心价值是约定大于配置内嵌Tomcat一键启动、自动装配、提供spring-boot-starter-web等一堆起步依赖让开发者把精力放在业务代码上而不是配置文件里。而且现在绝大多数企业用Spring Boot做后端用这个技术栈做毕设写进简历里说服力也更强。2.2 MyBatis Plus如何偷走80%的重复劳动MyBatis本身就是一个很优秀的ORM框架SQL和Java代码解耦复杂查询很好控制。而MyBatis Plus以下简称MP在它的基础上做了大量增强比如内置通用的Mapper方法单表CRUD基本不用写XML分页插件一套就生效不用手写方言兼容逻辑删除、自动填充字段、乐观锁插件全是开箱即用。这些能力在做仓库系统时几乎全用得上。最典型的是乐观锁插件库存表上加一个version字段更新时带上WHERE version ?CAS的思想用来防并发超卖代码里只需要给实体加一个Version注解。这是面试官最爱问的并发问题你已经有现成的项目落地经验可以讲。2.3 为什么建议用MySQL 8.0MySQL 8.0相比5.7在窗口函数、性能、默认字符集utf8mb4上都有升级而且现在云数据库基本都是8.0起步。字符集选utf8mb4可以支持生僻字和emoji商品名里万一有特殊字符不会出现插入失败的问题。有一点要注意数据库时区要设置成Asia/Shanghai连接串里也加上serverTimezoneAsia/Shanghai否则日期时间字段会差8小时这是最常见的坑之一。2.4 前端方案Thymeleaf还是前后端分离我明确建议如果以毕设为首要目标用Thymeleaf服务端渲染就够了甚至可以直接在模板里配合Bootstrap写后台管理界面。原因是前后端分离意味着你要同时维护Vue项目和Spring Boot项目接口联调、跨域、部署双份工作量直接翻倍。而Thymeleaf Bootstrap在Spring Boot里整合非常简单页面效果也拿得出手。如果你已经学过Vue非要用前后端分离那至少要用若依这类脚手架生成器来减少工作量否则论文还没写完光调试接口就够你喝一壶。毕设的本质是让你完整走一遍项目流程不是炫技。3. 数据库设计库存账实不一致的根源往往在一张表上数据库设计是仓库管理系统的灵魂。我审过不少毕设代码发现一个通病库存表和出入库记录表分离了但出入库明细表里居然没有存“操作前库存”和“操作后库存”。这就导致一个问题入库单删了之后库存怎么回滚根本无法精确还原历史状态。下面是核心表的推荐设计思路你可以直接照着建。3.1 商品与库存拆分维度要对商品表product保存的是静态信息商品编码、名称、分类ID、规格、单位、默认供应商ID、安全库存阈值、状态启用/停用。库存表stock保存的是动态信息商品ID、仓库ID、当前库存量、锁定库存量、可用库存量、版本号version、更新时间。两表通过商品ID关联但不要把库存字段直接冗余在商品表里——因为库存是仓库维度的同一个商品在不同仓库有不同数量冗余字段会导致无法扩展多仓库。这里引入一个概念锁定库存locked_stock。它表示已经被订单占用但尚未真正出库的数量。比如用户下单买了10件系统先扣减可用库存、增加锁定库存等订单发货完成再扣减锁定库存。这样既防止超卖又能处理“下单未支付”的场景。这个设计在毕设里哪怕只是简单实现老师也会眼前一亮。3.2 入库/出库单与明细表分离主表保存单据的公共信息单号、类型、供应商ID、仓库ID、业务日期、制单人、审核人、审核状态、备注明细表保存单据涉及的每个商品及数量、单价、批次号、生产日期、保质期。为什么要拆两张表因为一张入库单可能包含几十种商品主表和明细的数量是1:N关系拆开后主表可以独立统计单据数量明细表可以独立统计商品维度不会造成大量冗余。头表设计示例CREATE TABLE stock_in_main ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键ID, bill_no VARCHAR(32) NOT NULL UNIQUE COMMENT 入库单号, supplier_id BIGINT COMMENT 供应商ID, warehouse_id BIGINT NOT NULL COMMENT 仓库ID, bill_type TINYINT NOT NULL COMMENT 单据类型1采购入库 2退货入库 3调拨入库, bill_status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0待审核 1已审核 2已入库 3已作废, total_quantity INT DEFAULT 0 COMMENT 总数量, total_amount DECIMAL(12,2) DEFAULT 0 COMMENT 总金额, create_by VARCHAR(32) COMMENT 制单人, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, audit_by VARCHAR(32) COMMENT 审核人, audit_time DATETIME COMMENT 审核时间, remark VARCHAR(255) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT入库主表;明细表设计示例CREATE TABLE stock_in_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, main_id BIGINT NOT NULL COMMENT 关联主表ID, product_id BIGINT NOT NULL COMMENT 商品ID, quantity INT NOT NULL COMMENT 入库数量, unit_price DECIMAL(10,2) COMMENT 入库单价, batch_no VARCHAR(64) COMMENT 批次号, production_date DATE COMMENT 生产日期, expire_date DATE COMMENT 过期日期, INDEX idx_main_id (main_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT入库明细表;3.3 库存流水表一切库存变动的审计日志我的建议是任何库存变动必须同时往库存流水表插一条记录。流水表至少包含商品ID、仓库ID、变动类型入库/出库/盘点增减/调拨出/调拨入、关联单据号、变动前库存、变动数量、变动后库存、操作人、操作时间、备注。这样无论什么时候对账都可以通过流水表重放所有操作跟财务进销存对账的逻辑完全一致。有人可能觉得“这也太麻烦了吧每次操作要查一次当前库存再更新一次库存再插一条流水”。但实际上就是把库存更新的那一步放在同一个事务里多一次Insert操作系统性能完全能接受。麻烦一点换来的却是数据可溯源这个投资太值了。3.4 各表之间的外键关系我劝你别用物理外键很多教材还在教建表时加FOREIGN KEY但企业开发中基本不用物理外键而是在应用层维护逻辑关联。原因很简单物理外键会导致插入、更新时额外的约束检查高并发下影响性能而且一旦数据需要分库分表或者做归档物理外键会变成巨大的阻碍。所以上面的建表语句里我只用了索引INDEX没有用FOREIGN KEY。查询时通过JOIN操作解析表关系性能和灵活性都更好。4. 核心代码落地入库、出库与库存扣减并发方案这一章是整套系统的技术核心我会把关键代码逻辑拆开讲告诉你为什么这么写以及不同的写法分别会引发什么问题。4.1 Service层的三层结构不管什么模块Service层我建议都拆成三层入口方法控制事务边界做参数校验、幂等校验、权限校验核心业务方法具体的业务流转比如入库单审核后更新库存底层原子方法单表操作比如扣减库存的SQL。这样拆的好处是事务粒度可控一个入口方法对应一个完整业务任何一步出错整体回滚不会出现“扣了库存但没生成流水”这种半截子状态。4.2 入库操作事务校验流水一气呵成入库单审核通过后要做的操作逻辑上很简单把明细里的每个商品加到对应仓库的库存表中如果商品在该仓库还没有库存记录则新建已有则累加同时写库存流水。下面是核心Service代码的简化示例Transactional(rollbackFor Exception.class) public void auditStockIn(StockInMain main, ListStockInItem items) { // 1. 校验单据状态防止重复审核 if (main.getBillStatus() ! 0) { throw new BizException(当前状态不允许审核); } // 2. 循环处理每个商品明细 for (StockInItem item : items) { // 加锁查询库存这里用悲观锁锁住行防止并发 Stock stock stockMapper.selectByProductAndWarehouseForUpdate( item.getProductId(), main.getWarehouseId()); if (stock null) { stock new Stock(); stock.setProductId(item.getProductId()); stock.setWarehouseId(main.getWarehouseId()); stock.setCurrentStock(item.getQuantity()); stock.setLockedStock(0); stockMapper.insert(stock); } else { stock.setCurrentStock(stock.getCurrentStock() item.getQuantity()); stockMapper.updateById(stock); } // 3. 记录库存流水 saveStockFlow(item.getProductId(), main.getWarehouseId(), IN, main.getBillNo(), stock.getCurrentStock() - item.getQuantity(), // 变动前 item.getQuantity(), stock.getCurrentStock(), // 变动后 SecurityUtils.getUsername()); } // 4. 更新单据状态 main.setBillStatus(2); stockInMainMapper.updateById(main); }这段代码有两个细节值得注意rollbackFor Exception.class默认情况下Spring事务只回滚RuntimeException如果业务里抛的是自定义检查异常不加这个属性事务就不会回滚库存就扣错了selectByProductAndWarehouseForUpdate使用SELECT ... FOR UPDATE悲观锁锁住库存行保证同一时刻只有一个线程能修改这个商品的库存。4.3 出库操作的防超卖设计出库和入库逻辑上是对称的但有一个核心区别必须先校验可用库存当前库存减锁定库存不够就直接报错。在并发场景下如果用纯Java代码判断再更新两个线程同时读到库存为10同时判断充足同时执行减库存最后库存变成负的就是典型的超卖问题。解决思路有两种方案一悲观锁适合毕设和中小型系统直接用SELECT ... FOR UPDATE把库存行锁住其他线程必须等当前线程提交事务后才能读天然防超卖。缺点是并发高时锁等待会拉长但对仓库管理系统这个量级完全足够。方案二乐观锁适合面试加分MP的Version注解实现了乐观锁。更新时自动拼接SET stock stock - 10, version version 1 WHERE product_id ? AND warehouse_id ? AND version ?如果影响行数为0说明版本号已被其他事务改掉本次扣减失败重试或者提示用户库存不足。两种方案的取舍我在下面用表格总结一下对比项悲观锁FOR UPDATE乐观锁Version实现难度简单一个注解或SQL搞定简单加Version字段适用并发量低到中等秒杀类不建议中到高重试机制可接受代码侵入性需显式加锁SQL低MP自动处理失败处理串行等待不会失败冲突后需重试或报错推荐场景仓库管理系统、OA秒杀、电商下单对毕设项目我建议用悲观锁逻辑直观、答辩容易讲清楚如果你已经在简历里写了“熟悉并发编程”那在出库时用乐观锁然后讲一讲CAS的概念和ABA问题的处理会非常加分。4.4 单据编号生成别再用数据库自增当单号单据号是仓库系统的门面现实中入库单号通常有业务含义比如RK20250607001表示2025年6月7日第1笔入库单。用数据库自增ID当单号暴露了业务数据量也容易在合并数据时产生冲突。推荐做法是用年月日 当日自增序列public String generateBillNo(String prefix) { String dateStr LocalDate.now().format(DateTimeFormatter.ofPattern(yyyyMMdd)); String redisKey bill:seq: prefix : dateStr; Long seq redisTemplate.opsForValue().increment(redisKey); if (seq 1) { // 设置key的过期时间为当天24点 redisTemplate.expire(redisKey, Duration.ofDays(1)); } return prefix dateStr String.format(%03d, seq); }如果你不想引入Redis也可以用数据库表维护序列号但要记得加行锁防止重复生成。单号生成这个点虽然不起眼但做到位的同学非常少属于“答辩时老师问了你能接住”的细节题。4.5 盘点模块复盘时最能体现设计功底盘点业务比出入库复杂一点因为它的操作是“先盘后调”盘点时系统冻结当前库存录入实盘数量后系统根据差异自动生成盘盈盘亏单审核后再调整库存。如果简单做成“直接改库存数量”账面和实际永远对不上。建议的表结构是盘点单主表 盘点单明细表明细表里既有账面数量又有实盘数量差异数量实盘-账面审核盘点单时遍历明细生成库存调整流水并更新库存表。5. 权限与安全Spring Security在仓库系统中的落地姿势很多毕设项目把用户表里塞一个role字段然后在后端每个接口里if判断角色代码写起来很快但一旦角色多了、权限细分了这个系统就废了。Spring Security JWT是目前Java后端最主流的认证授权方案仓库管理系统完全能承载这套设计。5.1 认证流程和授权模型登录接口用AuthenticationManager做认证成功后生成JWT返回前端。前端请求时在Header里带Authorization: Bearer token后端用过滤器解析token、把用户信息放到SecurityContext里。授权时在接口上标注PreAuthorize(hasAuthority(stock:in:audit))方法级校验只有拥有该权限的用户才能执行。这样做的好处是权限控制精确到按钮级而且和业务代码完全解耦。我在实际开发中一共维护了三张权限相关的表用户表sys_user、角色表sys_role、菜单权限表sys_menu再加上sys_user_role和sys_role_menu两张关联表。前端根据登录接口返回的权限标识列表动态渲染“审核”“入库”“作废”等按钮后端在接口上再次校验防止有人绕过前端直接调接口。5.2 密码存储绝对不能用明文我评审过不少毕设用户表密码用明文存的比比皆是。一旦数据库泄露所有账号裸奔这在答辩时是硬伤。Spring Security内置了BCryptPasswordEncoder加盐哈希同一密码每次加密结果都不同安全性远高于MD5。用法非常简单Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }注册时passwordEncoder.encode(rawPassword)登录对比时passwordEncoder.matches(rawPassword, encodedPassword)。就这两行代码你就能在答辩时理直气壮地讲“我考虑了安全设计”。5.3 操作日志用AOP统一切面实现仓库系统的审计要求很高谁在几点审核了哪个单据、修改了什么数据都要有记录。千万不要在业务代码里手动一行一行地调日志Service正确做法是用AOP切面统一拦截Aspect Component public class OperationLogAspect { Around(annotation(operationLog)) public Object around(ProceedingJoinPoint point, OperationLog operationLog) throws Throwable { long start System.currentTimeMillis(); try { Object result point.proceed(); saveLog(operationLog.module(), operationLog.action(), JSON.toJSONString(point.getArgs()), SUCCESS, System.currentTimeMillis() - start); return result; } catch (Exception e) { saveLog(operationLog.module(), operationLog.action(), JSON.toJSONString(point.getArgs()), FAIL: e.getMessage(), System.currentTimeMillis() - start); throw e; } } }这样业务代码里只需要打一个OperationLog(module 入库管理, action 审核入库单)注解日志自动记录大大减少了侵入性。这里的JSON序列化要注意排除密码字段防止日志里出现敏感信息。6. 前后端交互与报表统计的实用细节仓库管理系统离不开对数据的统计和分析这里讲两个实操性极强的点一是前后端交互时的参数校验二是报表统计的SQL怎么写才高效。6.1 后端参数校验Bean Validation替代手写if很多同学在Controller里用一长串if参数校验代码又臭又长。Spring Boot集成了hibernate-validator在实体类上直接标注注解即可public class StockInMainDTO { NotNull(message 仓库ID不能为空) private Long warehouseId; NotBlank(message 单据类型不能为空) private String billType; Valid private ListStockInItemDTO items; }Controller里加Validated注解参数不合法时框架自动抛异常再配一个全局异常处理器统一返回JSON提示。既保证了数据安全又让代码清爽到可以当简历亮点。6.2 库存汇总报表的SQL优化思路报表模块最容易出现的问题就是SQL写得稀烂。比如统计某个时间段的商品入库汇总有人会先查所有入库单再在Java内存里循环累加——数据量一大系统直接卡死。正确的做法是让数据库帮你算SELECT p.id AS product_id, p.product_name, SUM(sii.quantity) AS total_in_quantity, SUM(sii.quantity * sii.unit_price) AS total_in_amount FROM stock_in_item sii JOIN stock_in_main sim ON sii.main_id sim.id JOIN product p ON sii.product_id p.id WHERE sim.bill_status 2 AND sim.create_time BETWEEN #{startTime} AND #{endTime} GROUP BY p.id, p.product_name ORDER BY total_in_quantity DESC这条SQL利用了bill_status2过滤掉未入库或已作废的单据通过JOIN关联查出商品名称GROUP BY按商品聚合。核心逻辑在数据库完成Java层只拿到最终结果数据量大时性能差距是指数级的。再配合MyBatis Plus的分页插件报表接口轻松跑完。6.3 数据导出阿里巴巴EasyExcel节省一天工作量论文里要贴系统测试截图实际场景中用户也需要导出Excel报表。我推荐直接用阿里巴巴的EasyExcel几百行数据调用一个write方法就能输出到浏览器response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setCharacterEncoding(utf-8); EasyExcel.write(response.getOutputStream(), StockInReportVO.class) .sheet(入库明细) .doWrite(dataList);注意设置响应头的文件名时要做URL编码否则中文文件名在浏览器里会乱码。EasyExcel的底层是SAX解析内存占用远低于POI这一点也值得在答辩时提一下说明你关注了性能。7. 部署上线与常见排坑本地跑通不难难的是不踩坑最后一个部分我总结一下这套系统的部署要点和最容易踩的坑。毕设答辩通常需要现场演示如果你的项目在本机跑不起来或者跑起来一团乱麻写得再好也白搭。7.1 环境版本匹配表先给出一份我推荐的环境组合版本匹配能帮你省掉大量相互兼容的烦恼软件推荐版本避坑说明JDK8 或 11Spring Boot 2.7.x用JDK8最稳JDK17需配Boot 3.xMySQL8.0驱动用com.mysql.cj.jdbc.DriverRedis5.x及以上单机版即可Windows可以用tporadowski/redis替代Node.js/Vue不必须若用前后端分离建议Vue2 Node14Maven3.6.3以上仓库源建议换成阿里云镜像有一个点我特别强调一下Spring Boot版本和JDK版本强绑定。如果你电脑装了Java17再用Spring Boot 2.6.x编译时各种报错解决方案要么换成JDK8环境变量要么升Spring Boot到3.x。改环境变量这件事看起来简单但每年的毕业季都有大量同学卡在这里。7.2 本地启动的四大步初始化数据库在Navicat或命令行执行init.sql脚本注意先创建数据库实例名wms_system再执行建表语句字符集选utf8mb4。修改配置文件修改application.yml里的数据库连接串、账号密码、Redis地址。如果Redis连接失败系统不会直接挂掉但单号生成、验证码功能都会异常。Maven打包在项目根目录执行mvn clean package -DskipTests如果网络不好下载依赖失败检查Maven仓库地址是否配置了阿里云镜像。启动和验证运行主类打出的jar包访问http://localhost:8080/看到登录页就说明环境OK。如果端口被占用在配置文件里改server.port。7.3 高频报错Top 4及排查思路这里我把历届学弟学妹遇到最多的四类问题列出来按频率排个序Failed to configure a DataSource大概率是application.yml放在src/main/resources之外或者数据库连接串写错了。检查配置文件位置和URL里的useSSLfalse参数。Invalid bound statement (not found)MP的Mapper接口和XML文件没对应上。检查Mapper接口的全限定名、XML命名空间以及mapper-locations配置。JWT token过期导致接口401测试时把token有效期调长生产再调短这个顺序别搞反了。时间差8小时在数据库连接串末尾加上serverTimezoneAsia/Shanghai同时MySQL启动参数里加上-Duser.timezoneAsia/Shanghai两个地方缺一不可。7.4 答辩演示的加分小技巧演示系统前提前准备几组商品数据、供应商数据、用户数据。演示顺序建议是登录 - 新增商品 - 创建采购入库单并审核 - 查询实时库存看数量增加 - 创建销售出库单并审核 - 再查库存看数量减少 - 打开库存流水看每一步的变动记录 - 用无权限账号演示接口被拦截。这一套流程走下来业务闭环和权限设计全都覆盖到了老师想刁难你都找不到切入点。写在最后从“能跑”到“讲得清楚”才算真正做完仓库管理系统看起来是个老题目但我带的学员里有人靠它拿到了电商公司的offer也有人答辩时被老师问得哑口无言。差别不在于代码量多少而在于你是否想清楚了每一个设计背后的原因——为什么库存要单独建表、为什么出入库必须走单据流、为什么并发扣库存要用锁、为什么密码不能存明文。这些问题的答案就是面试官和答辩老师真正想听的东西。如果你正在做这套系统不妨在跑通代码之后再回头做一件事把每个核心表之间画一张关系图然后对着图把“一张采购入库单从创建到审核再到库存增加”的完整链路讲给自己听。讲不流畅的地方就是你需要再补课的地方。这样折腾一遍这个毕设项目才真正长在你身上了。
