最近在带学生做毕业设计好几个选了这个方向核心标的都是“基于JAVA的卷烟厂库存管理系统”或者换个说法叫“基于Spring Boot的卷烟厂原料与成品库存精细化管控平台”。从题目看这就是典型的企业级Web系统开发技术栈锁定Java Spring Boot业务场景落在烟草行业的原料和成品库存管理上。说实话这个题目选得挺聪明的。一方面Spring Boot是目前Java后端开发的事实标准学了能用另一方面烟草行业的库存管理比普通超市进销存要复杂得多——烟叶原料要按等级、年份、醇化期管理成品烟要按牌号、批次、件烟条码追踪还要考虑片烟、烟梗、香精香料这些辅料的保质期问题。把这些业务逻辑理清楚、做出来既有技术含量又有行业深度答辩时也有的讲。我先把这个系统该做成什么样、技术方案怎么选、核心代码怎么落地、踩过哪些坑一条一条给你捋清楚。不管你是自己要写这个毕设还是想把它当企业级项目的练手案例这篇文章应该都能帮上忙。1. 系统定位与功能架构拆解1.1 卷烟厂库存管理到底在管什么很多人一听“库存管理”就以为是简单的商品增删改查这在小卖部系统里成立但在卷烟厂完全不是一回事。我实际了解过烟草行业的仓储流程这里面的复杂度主要体现在三个维度。第一个维度是原料的层次结构。烟叶原料不是单一商品它分为原烟、片烟、梗丝、膨丝、再造烟叶等不同形态每种形态又按产地、等级比如X2F、C3F这种烟草行业专属等级代号、年份、醇化周期分成多个批次。不同批次的烟叶在配方里的用途不同投放优先级也不同。这让原料库存必须做到“批次级”管理而不是简单合并同类项。第二个维度是成品的多样性。成品烟不是一种而是按品牌、规格软盒、硬盒、细支、中支、焦油含量、口味等多个属性形成SKU矩阵。而且每箱烟有唯一的件烟条码出库时要能追溯到具体是哪个生产批次、哪个质保时段生产的。第三个维度是物资流转的复杂性。卷烟厂的仓库不是只有原料和成品还有大量辅料烟用纸箱、滤嘴棒、香精香料、备品备件。以滤嘴棒为例它有25mm、30mm等多种规格还要考虑丝束的来料批次。如果只做原料和成品两个模块当然能满足毕业设计的核心得分点但要做好主数据和生产计划的衔接就得把辅料作为一个独立模块考虑进去。所以这个系统的核心目标可以概括为让原料、成品、辅料的入库、存储、出库、盘点全过程可追踪、可校验、可分析同时打通订单到出库的协同链路。1.2 基于Spring Boot的模块划分与设计逻辑我给学生推荐的分层模块是这样的基础数据模块管理牌号信息也就是成品烟的SKU、原料等级字典、仓库货位、往来单位供应商、销售客户、烟叶等级标准。入库管理模块原料入库含质检记录、成品入库生产下线入库、辅料入库、采购退货等。出库管理模块原料领料出库车间投料、成品销售出库、辅料领用、调拨出库等。库存管理模块现存量查询、批次台账、货位库存、移库操作、库存盘点、安全库存预警、效期预警。订单协同模块销售订单创建、订单审核、订单状态跟踪、根据订单自动生成出库单。系统管理模块用户、角色、权限、菜单、操作日志。模块划分的核心思想是“单据驱动库存”。也就是说所有库存数量的变化必须由业务单据触发不允许直接修改库存表。比如ERP里常见的逻辑创建入库单→审核通过→库存自动增加创建出库单→审核通过→库存自动扣减。这样做的好处是一旦库存数据出现异常可以顺着单据流往回查定位是哪个环节出了问题。在技术结构上Spring Boot项目内部要严格分层Controller层只负责接收参数和返回结果Service层处理业务逻辑Mapper层操作数据库。我见过很多学生把所有逻辑塞进Controller里代码倒是能跑但一涉及事务就会出各种诡异问题。我们后面讲到事务边界和排查问题时你会看到分层的必要性。1.3 系统能解决的实际痛点与使用对象这套系统建设完后解决的问题不是纸上谈兵而是车间和仓库每天实打实的痛苦。第一解决数据滞后问题。之前用Excel管库存晚上生产结束第二天上午才能统计出昨天的原料消耗和成品入库量数据永远滞后。这个系统能做到实时更新只要你完成出库审核车间数据员当时就能看到最新的投料库存状态。第二解决批次追溯难题。烟草行业对质量追溯的要求非常严格如果某批原料抽检发现农药残留超标要立刻锁定这批烟叶被投到了哪些牌号、哪些批次的产品里。手工记录基本做不到这种追溯而批次化的数据库表设计可以做到“一键倒查”。第三解决纸质单证易丢失、易漏登的问题。仓库管理员收发货时用PDA或者电脑做单单据电子化月底盘点时再也不用抱着一大摞纸质单据和账本来回翻。使用对象上除了作为毕业设计展示项目里涉及的Web端管理后台、角色权限和报表分析完全可以拿来做Java后端岗位的求职项目——因为它有完整的业务闭环面试官问业务问题你也答得上来。2. 关键技术选型与数据库模型设计2.1 为什么选Spring Boot而不是Spring MVC这个问题几乎每次答辩老师都会问我建议你在文档和答辩PPT里写得明明白白。Spring Boot相对于传统Spring MVC核心优势是自动配置和“开箱即用”。你引入spring-boot-starter-web依赖内嵌Tomcat写一个SpringBootApplication启动类一个简单的Web服务就跑起来了不需要再去配置一大堆XML文件。这意味着你可以把精力放在业务功能实现上而不是纠结配置文件怎么少写一个bean。当然Spring Boot并不是省掉了Spring的IOC容器和AOP能力它底层还是Spring Framework只是帮你做了大量自动化和约定优于配置的封装。在项目里我们用Transactional管理事务用Spring Security做登录认证和权限控制用Spring AOP实现操作日志记录这些机制都是Spring的看家本领。对于毕业设计或小团队内部系统Spring Boot MyBatis Plus MySQL Redis这套组合开发效率最高。Java 17或Java 21都行稳定优先选JDK 8到JDK 17之间的版本别急着上Java 21的虚拟线程——这个功能确实香但如果你不熟悉反而会在毕业设计里给自己挖坑。当然如果只是Spring Boot 3.5 Java 21跑个demo级别的系统虚拟线程的兼容性问题不大但我个人建议还是求稳。2.2 数据库表结构设计与核心字段说明数据库是整个库存管理系统的地基。我直接把核心表结构和字段设计思路列出来你照着建库就行。第一张是原料库存表raw_material_stock。这张表做批次级管理核心字段包括原料ID、原料编码、原料名称、等级编码如C3F、产地、年份、形态类型原烟/片烟、批次号、仓库ID、货位ID、数量单位公斤、质检状态、入库时间。批次号是全局唯一的由系统生成比如按日期加流水号生成。第二张是成品库存表finished_product_stock。字段包括成品ID、牌号编码、牌号名称、规格类型硬盒/软盒/细支、包装数量如每条10包、每箱50条、批次号生产批次、质保截止日期、仓库ID、货位ID、件烟条码、数量。成品库存的最小单位可以设计成“件”或“条”具体看业务流程。我建议同时保留件数和条数两个字段出库时可整件可拆条。第三张是出入库单据主表和明细表。主表stock_document存储单据编号、单据类型、业务方向入库/出库、关联订单号、往来单位、仓库ID、操作人、审核人、制单时间、审核状态等字段。明细表stock_document_item存储单据对应的具体物料、批次、货位、数量。这样设计的好处是同一张入库单可以包含多个不同批次的烟叶符合实际业务中一车原料混装多批次的场景。第四张是订单表sales_order。字段包括订单编号、客户编码、客户名称、牌号编码、牌号名称、订货数量、已出库数量、订单状态待审核/已审核/已分配/已完成、下单日期、期望交货日期、订单金额。订单状态机是整个订单协同模块的核心后面在业务实现里我再展开讲。其他辅助表还包括仓库表、货位表、盘点记录表、库存变动流水表stock_change_log、用户角色权限表。其中库存变动流水表非常关键每笔库存变动都要插入一条流水记录变动前数量、变动后数量、变动原因、关联单号、操作人。注意库存流水和单据明细是“审计追踪”的基础千万别省。我见过有人图省事只维护库存表结果库存对不上账时完全无从排查。有了流水表所有的库存异动都在掌控之内。2.3 核心数据流与库存数据一致性方案这个系统里最核心的数据流有两条。第一条是采购入库流采购合同/到货通知 → 质检记录 → 创建原料入库单 → 审核通过 → 插入库存明细/更新库存总量 → 记录库存流水。烟草公司对于原料烟叶的质检非常严格抽样不合格整批可能拒收所以入库单要有质检状态字段质检不合格的单据不允许审核入库。第二条是销售出库流销售订单创建 → 订单审核 → 库存分配锁定库存 → 生成出库单 → 出库单审核 → 库存扣减 → 更新订单已出库数量 → 记录库存流水。注意这里涉及一个“库存锁定”的概念创建出库单时应该先把所需数量的库存锁定状态变为已分配但还没真正扣减等出库单审核后才执行实际的库存扣减。这样防止A订单刚创建B订单也看到同一批库存并尝试出库造成超卖。在技术上库存扣减这个动作建议采用“前置校验 数据库乐观锁或悲观锁”。最简单可靠的做法是扣减时使用UPDATE ... WHERE id ? AND quantity 所需数量这样的条件更新语句MySQL会基于行锁保证并发安全。如果更新返回的影响行数为0说明库存不足需要回滚并提示用户。这种方式比先SELECT再UPDATE更安全因为它在数据库层面保证了原子性。3. 从零实现前端页面、后端接口与核心业务代码3.1 技术栈补充与开发环境搭建从零开始做这个系统开发环境建议如下IDEIntelliJ IDEA社区版也够用但专业版对Spring Boot的支持更好JDKJDK 17Spring Boot 3.x要求JDK 17起步建议使用构建工具Maven 3.8数据库MySQL 8.0Navicat或DBeaver做客户端缓存Redis 6.x用于存token实现单点登录、缓存字典数据前端框架Vue 3 Element Plus Vite后台管理界面标准组合或者直接用Thymeleaf做服务端渲染也可以简化开发量。前端如果时间紧只做Web端管理后台就足够撑起整个项目的完整度。脚手架推荐直接去 Spring Initializr 生成项目勾选Spring Web、MyBatis Framework、MySQL Driver、Spring Security、Validation依赖。项目生成后在application.yml里配置数据源、Redis和MyBatis的mapper扫描路径。3.2 后端基础工程结构规范我一贯偏爱按业务模块而不是按技术层来分包这样新手拿到项目更好理解com.cyg.inventory ├── common // 公共类统一返回结果、异常处理、工具类 ├── config // 配置类Redis配置、Security配置、MyBatisPlus配置 ├── controller // 控制器 │ ├── rawmaterial │ ├── finishedproduct │ ├── stock │ ├── order │ └── system ├── service // 业务接口和实现 │ ├── stock │ ├── order │ └── ... ├── mapper // 数据库操作层 ├── entity // 实体类 ├── dto // 数据传输对象 └── vo // 视图对象统一返回结果类建议设计成泛型ResultT包含code、message、data三个字段。Controller返回时统一包装。这样做有两个好处前端处理接口响应逻辑统一不需要每个接口单独写判断错误信息也能标准化调试起来轻松很多。3.3 核心功能模块详细实现入库、出库、盘点入库业务建议设计成五个步骤的原子事务前端提交入库单基本信息仓库、往来单位、单据类型、经办人等提交入库明细物料、批次、数量、货位后端校验批次是否存在、货位是否有效、数量是否为正数且符合精度要求主表状态设为待审核明细写入单据明细表审核节点通过后依次执行为库存表增加数量、新增库存变动流水、回填单审核状态代码层面这一步尤为关键。我先提供一个简化的Service核心方法你能直观理解其中逻辑Service public class InStockServiceImpl implements InStockService { Autowired private StockRecordMapper stockRecordMapper; // 库存流水 Autowired private RawMaterialStockMapper rawMaterialStockMapper; Autowired private StockDocumentMapper stockDocumentMapper; Autowired private StockDocumentItemMapper stockDocumentItemMapper; Override Transactional(rollbackFor Exception.class) public void auditInStock(Long documentId) { // 1. 校验单据状态防止重复审核 StockDocument doc stockDocumentMapper.selectById(documentId); if (doc null) { throw new BizException(单据不存在); } if (!待审核.equals(doc.getStatus())) { throw new BizException(单据状态不允许审核); } // 2. 获取单据明细列表 ListStockDocumentItem items stockDocumentItemMapper .selectList(new QueryWrapperStockDocumentItem() .eq(document_id, documentId)); // 3. 逐条处理增加库存并记录流水 for (StockDocumentItem item : items) { RawMaterialStock stock rawMaterialStockMapper .selectByBatchAndLocation(item.getBatchNo(), item.getLocationId()); if (stock null) { throw new BizException(库存批次记录不存在 item.getBatchNo()); } // 库存增加注意这里可以加数据库乐观锁防止并发修改 int rows rawMaterialStockMapper.increaseQuantity( stock.getId(), item.getQuantity(), stock.getQuantity()); if (rows 0) { throw new BizException(库存更新失败请重试); } // 记录库存流水 StockChangeLog log new StockChangeLog(); log.setMaterialId(stock.getMaterialId()); log.setBatchNo(item.getBatchNo()); log.setChangeType(入库); log.setBeforeQty(stock.getQuantity()); log.setAfterQty(stock.getQuantity() item.getQuantity()); log.setDocumentNo(doc.getDocumentNo()); log.setOperateUserId(doc.getAuditUser()); stockChangeLogMapper.insert(log); } // 4. 更新单据状态 doc.setStatus(已审核); stockDocumentMapper.updateById(doc); } }那行单独的increaseQuantity要落到Mapper的XML里采用条件更新来保证并发安全update idincreaseQuantity UPDATE raw_material_stock SET quantity quantity #{increaseQty}, update_time NOW() WHERE id #{id} AND quantity #{expectQty} /update这里用quantity 期望值作为乐观锁条件如果期间有别的操作改了数量UPDATE影响行数为0事务回滚业务提示重试。实测下来这个方案在高并发下既简单又可靠比在Service层用synchronized锁住一堆代码要高效得多。出库业务恰好相反扣减库存前必须校验可用库存是否充足。我建议出库单审核时先执行这样一条条件更新UPDATE raw_material_stock SET quantity quantity - #{outQty} WHERE id #{id} AND quantity #{outQty}影响行数为0就是库存不足或者库存记录被其他并发事务改了直接抛业务异常事务整体回滚。订单分配库存时也要查询“可用库存”可用库存等于总库存减去已锁定数量这样才不会被其他出库单抢走同一批货。盘点业务也不可跳过。建议做“盘点单 → 盘点明细录入 → 盘点差异审核 → 生成盘盈盘亏调整单”四步流程。盘点时系统自动冻结该货位的出入库操作或者允许录入差异但锁定盘点期间的单据避免边盘边变。差异审核通过后要把差异数量反映到库存表和库存流水里保留完整的盘点差异记录。3.4 订单协同的关键实现思路订单协同是这个系统区别于普通进销存的一大亮点答辩时值得多花篇幅展示。销售订单的状态机建议设计为待审核 → 已审核 → 部分出库 → 已完成 → 已取消。用户在订单管理页面创建订单后销售经理审核通过物流部门看到已审核订单后点击“生成出库单”系统自动根据订单明细生成一条关联的出库单仓储管理人员对出库单审核出库出库完系统自动更新订单的已出库数量当已出库数量等于订单数量时订单状态自动变更为已完成。在数据库层面订单表和出库单表通过order_no字段关联。出库单明细冗余存储了牌号编码、牌号名称、数量、批次号等信息做到“单据流可追溯、数据不丢失”。订单里的“库存精准锁定”功能同样值得重点实现创建出库单的时候系统根据指定的仓库和货位按“先进先出”FIFO原则自动分配批次库存。这个在SQL里可以用排序实现先查所有可用批次按照入库时间升序排列依次锁定前几个批次直到数量满足需求。// 简化示例按先进先出锁定库存批次 ListFinishedProductStock batchList finishedProductStockMapper .selectAvailableBatches(brandCode, warehouseId, orderQty); int allocatedQty 0; for (FinishedProductStock stock : batchList) { int need orderQty - allocatedQty; int take Math.min(need, stock.getAvailableQty()); // 执行库存锁定 update... allocatedQty take; } if (allocatedQty orderQty) { throw new BizException(可用库存不足无法生成出库单); }这个FIFO逻辑在答辩时讲出来老师会觉得你懂业务而不是只会“增删改查”。3.5 前端页面与后端接口的完整交互设计思路前端用Vue 3 Element Plus的话我建议把页面分成数据看板、业务操作、信息查询、系统配置四大类。数据看板放库存总览仪表盘统计今日入库量、今日出库量、库存总值、原料库存预警数量等业务操作分别是入库单、出库单、盘点单、移库单的新建与审核页面信息查询对应库存现存量查询、批次台账查询、出入库流水查询系统配置对应仓库货位管理、供应商/客户管理、用户权限管理。接口设计遵循RESTful风格。核心接口如下POST /api/stock/raw/inbound # 新建原料入库单 POST /api/stock/raw/inbound/audit # 审核原料入库单 POST /api/stock/raw/outbound # 新建原料出库单领料 POST /api/stock/raw/outbound/audit # 审核原料出库单 GET /api/stock/raw/current # 原料现存量查询 GET /api/stock/raw/batch # 原料批次台账 POST /api/stock/finished/inbound # 成品入库生产下线 POST /api/stock/finished/outbound # 成品出库销售 GET /api/stock/finished/current # 成品现存量查询 POST /api/order/sales/create # 创建销售订单 POST /api/order/sales/audit # 销售订单审核 POST /api/order/sales/toOutbound # 订单生成出库单 GET /api/order/sales/list # 订单列表含状态查询 POST /api/stock/check/create # 创建盘点单 POST /api/stock/check/audit # 盘点差异审核前端的鉴权方式建议用JWT配合Spring Security。用户登录后后端返回一个token前端请求头带上Authorization: Bearer token后端通过Security过滤器链校验token有效性和角色权限。这个方案是现在企业级前后端分离项目的主流做法写进文档和答辩PPT里都很有说服力。4. 异常场景处理与项目优化经验4.1 库存超卖与并发扣减怎么防库存管理系统最怕的就是超卖。两个管理员同时看到一个批次还有2000公斤烟叶分别给两个车间各开1800公斤的出库单如果代码是先查库存再判断够不够两次查询看到的都是2000公斤随后扣减就会出现库存变负的情况。前面已经提过正确做法是用条件更新SQL确保原子性。我再拓展一个场景如果出库单要跨多个批次扣减事务跨度更长还得注意数据库事务隔离级别。MySQL默认的Repeatable Read下多个事务同时扣减同一批次的库存时“行锁”会串行化更新操作不会出现幻读导致的超扣。只要UPDATE语句里带上“WHERE quantity 需要扣减的量”作为条件并发安全就能保证。如果Redis用上了也可以先用Redis的分布式锁或者Lua脚本做库存预扣再异步落库。但这个方案复杂度高业务逻辑容易绕晕单体项目里直接用数据库条件更新是最好的选择简洁、可靠、易解释。4.2 报表统计与性能优化怎么做库存管理系统到后期一定要上报表功能否则评估说不完整。建议至少实现四个报表日出入库汇总表按日和仓库维度、原料库存结构分析表按等级/年份/产地维度、成品库存库龄分析表按批次和入库时间算库龄、安全库存预警表低于阈值的物料列表。性能方面有两个最容易踩的坑。第一个坑是列表查询没有做分页数据量稍大页面直接卡死。MyBatis Plus自带分页插件配置好PaginationInnerInterceptor所有列表分页查询必须走分页插件。第二个坑是报表聚合查询用SELECT *然后Java里for循环过滤这是灾难级别的性能劣化。正确做法是写SQL聚合语句用GROUP BY、SUM、COUNT直接算出结果。-- 按仓库原料等级统计可用库存 SELECT warehouse_id, grade_code, SUM(quantity) AS total_qty FROM raw_material_stock WHERE status 可用 GROUP BY warehouse_id, grade_code;索引优化也不能忽略。库存表查询频率最高的是按仓库ID、货位ID、批次号、物料编码查询所以这几个字段要建联合索引。比如(raw_finished_stock表的warehouse_id, location_id)联合索引能让货位库存查询走索引避免全表扫描。订单表重点在order_no、status上建索引。4.3 Spring Boot项目里必踩的坑与排查方法第一个高频坑是MyBatis的XML文件放错目录导致找不到Mapper。用Spring Boot MyBatis时如果XML文件放在resources下要在application.yml里配置mapper-locations: classpath:/mapper/*.xml。如果XML放在java包目录下有的人喜欢这样还得额外在pom.xml里声明resources包含xml文件否则打包后XML不会进入classpath启动直接报Invalid bound statement。第二个高频坑是事务不生效。最常见原因是同类内部方法调用比如OutStockService的createOutStock方法调用了同类的auditOutStock方法即使auditOutStock标了Transactional也不会生效因为Spring的事务是基于动态代理实现的内部调用绕过了代理。解决办法如果是自调用拆分成不同的Service互相调用或者自己注入自身代理类AopContext.currentProxy()。注意Spring Boot 2.6.0默认开启EnableAspectJAutoProxy的exposeProxyfalse想用AopContext得先开启配置。第三个坑是Spring Security的过滤器链导致接口401。一些学生配置Security后忘记放行登录接口和静态资源前端页面死活调不通接口排查老半天发现是Security拦截了。放行配置正确的是http.authorizeHttpRequests() .requestMatchers(/api/auth/login, /doc.html, /webjars/**, /css/**, /js/**).permitAll() .anyRequest().authenticated();第四个坑是MyBatis Plus的字段自动填充不生效。创建时间、更新时间这类字段如果数据库允许为NULL代码里没有设置值插入后查询出来就是NULL。统一用MyBatis Plus的自动填充功能TableField的fill策略 MetaObjectHandler实现比每张表手动setTime要省心得多。第五个坑是MySQL的时区问题导致日期差8小时。连接字符串里加上serverTimezoneAsia/Shanghai并把JDBC驱动版本升级到8.0.30以上一般就能解决。数据库端建议所有时间字段用datetime类型代码里统一用LocalDateTime接受。4.4 线上环境配置与性能压测建议如果这个系统不只是交作业还想走到线上试运行Java的JVM参数建议加上最大堆内存限制避免默认堆内存大小导致服务器负载飙升。java -Xms512m -Xmx1024m -jar inventory-system.jar --spring.profiles.activeprodMyBatis打印SQL在生产环境一定要关掉不然日志量大到恐怖。配置里把mybatis-plus.configuration.log-impl设置为org.apache.ibatis.logging.slf4j.Slf4jImpl然后日志级别设成INFO即可关闭SQL打印。系统上线前至少做一轮基础压测推荐用Apache JMeter模拟50个并发用户重点关注两个场景一是并发提交出库审核时的库存扣减正确性二是库存现存量查询接口在百万级数据量下的响应时间。以我实测经验不建索引的现存量查询库存表数据到50万行时响应时间会从30ms掉到800ms以上建索引并优化SQL后能稳定在100ms以内。5. 毕业设计展示与个人收获页面展示和系统演示这里重点建议把“数据大屏”做出来——用ECharts做一个库存总览看板页面显示饼图各牌号成品库存占比、折线图近7天出库趋势、柱状图各仓库库存量以及预警信息列表。这个页面在系统演示时视觉效果最直观答辩老师打开第一眼就会被吸引住分数一般不会低。ECharts本身是开源免费的集成方式也简单npm安装echarts后按文档配置一个div容器就可以出图。至于整个项目做完之后的收获我的感受是Spring Boot的上手成本确实很低但真正拉开水平差距的是业务建模能力和数据一致性设计。把一张入库单拆成主表和明细表、把库存变动的每一步都通过事务和流水串起来、把订单到出库的状态机理清楚——这些看不见的“内功”才是一个Java开发工程师最值钱的部分。这套系统做完你不仅会写接口还要能理解业务流程面试官问起“你来说说你们系统怎么防止并发超卖”时你能讲出“条件更新 数据库行锁 库存流水”这套组合拳工作Offer基本就稳了一半了。最后说个小技巧。做演示的时候一定先提前把库存数据造好别用空数据库。该铺垫的库存数据要提前铺满几个仓库、几十个货位、几十个牌号的成品批次、不同年份的原料批次。界面上一眼能看到的丰富数据比你现场敲半天的代码有用得多——毕竟答辩老师看的是你的系统能力和业务理解不是看你打字速度。
阅读完成 · 觉得有帮助?