做了几年Java后端,也带过不少毕业设计项目,我太熟悉“SpringBoot 仓储管理系统”这个选题了。你搜一下“SpringBoot、仓储管理系统、智能仓库、库存管控、物料追踪系统”这几个关键词,跳出来的基本都是同一类东西:用 SpringBoot 写后端、Vue 写页面,做一套能入库、出库、盘点、查库存的后台系统。这实际上是个好选题,业务场景清晰,功能点也不像电商系统那么绕,特别适合用来展示你对工程结构、数据库设计、事务控制、权限处理的理解。但这系统想做成“能答辩、能演示、能写出东西”的程度,光靠抄一个 demo 是不够的,你得把自己当成一个真正在给仓库干活的人,而不是单纯在做 CRUD。
我打算把这套系统的完整落地过程拆开讲一遍,从需求梳理、表结构设计、后端接口实现,到前端联调、打包部署,以及那些不跑一遍绝对发现不了的坑,全按我平时带项目的思路来。这篇文章适合正在做相关毕设的同学,也适合打算拿这个项目去面试的初级 Java 开发。如果你已经有 SpringBoot 基础,可以直接跳到第 3 章看实现细节;如果还在选型阶段,从头看也不吃亏。
1. 项目整体设计与需求拆解
仓储管理系统表面上就是管“货进、货出、货在哪”,但它真正的复杂度在于:同一件物料可能分批次、分库位存放,入库单有多个状态,出库时要考虑先进先出,盘点时发现账实不符还要生成差异单。毕业设计如果只做一张物料表和一张出入库记录表,那答辩时基本会被老师问住。所以第一步,别急着写代码,先把手里的业务场景理顺。
1.1 仓库作业的核心场景与功能清单
我把最常见的仓储作业拆成几条主流程:
一是入库流程。供应商送货过来,库管员根据采购单或到货通知创建入库单,填写物料、数量、批次号、存放库位,提交后由主管审核,审核通过才真正增加库存。这里关键点是“审核前不能动库存”,否则单据作废后数据就乱了。
二是出库流程。生产领料或销售发货时,创建出库单,指定物料和数量,系统自动按先进先出或指定批次扣减库存。如果库存不足,要么拦截,要么允许负库存但强制加标记。我在项目里默认拦截,比较安全。
三是盘点流程。库管员定期对某个库位或某批物料进行实盘,录入实际数量,系统与账面数量对比,生成盘盈盘亏明细,经审核后调整库存台账。
四是调拨与库位管理。物料从A库位移动到B库位,需要调拨单来记录轨迹。这个功能如果时间紧可以做成简单的“移库”操作,但表设计上起码要留出 field。
还有一类是“非作业但必须有的”:物料档案管理、供应商/客户档案、用户与权限、操作日志、库存预警、统计报表。毕设项目做到前五个功能,再配合一个不错的界面,已经能撑住场面了。
1.2 为什么选 SpringBoot + Vue 这套组合
作为毕设,技术选型不是越炫越好,而是“你讲得清楚、老师听得懂、代码能跑通”。SpringBoot 加 Vue 是主流答案:后端用 SpringBoot 是因为它极大简化了 Spring 配置,自动装配帮你搞定大部分 bean 管理,你只要写业务逻辑就行;前端用 Vue3 加 Element Plus,是因为它组件丰富,表格、表单、弹窗、分页都现成,几天就能搭出一套后台管理界面。
我看过太多同学一上来就整微服务、分布式事务、消息队列,结果自我介绍都讲不利索。毕业设计考察的是基本功,不是你背了多少中间件名字。SpringBoot 单机 + MySQL + Redis(可选)就足够了。如果非要加亮点,我会选择加一个定时任务做库存呆滞预警,或者用 WebSocket 推送库存不足通知,这些在答辩时都是非常有效的加分项。
1.3 技术栈版本取舍:别被“太高版本”坑到
这里专门提醒一句:用 SpringBoot 的时候,不要盲目追求最新版本。网上很多教程和依赖都跟不上新版节奏,尤其当你在 IDEA 里新建项目时默认拉到的版本可能比较高(比如 SpringBoot 3.x 配 JDK 17),到时候整合 MyBatis-Plus 或某些老的第三方依赖时,会出现各种诡异的兼容性问题。
我现在的习惯是:如果按最新稳定生态,用 SpringBoot 3.0+ 配 JDK 17 没问题;但如果想抄网上的代码,多数资料还是 SpringBoot 2.5~2.7 配 JDK 8,你照搬时会更顺利。具体版本我一般这样选:
- JDK 8 + SpringBoot 2.7 + MyBatis-Plus 3.5.x + MySQL 8.0;
- 或者 JDK 17 + SpringBoot 3.2 + MyBatis-Plus 3.5.5+(注意新版里很多包名和自动配置类有调整)。
如果你看到项目启动报错说“package javax.servlet does not exist”或者“ClassNotFound”,大概率就是 SpringBoot 版本和 servlet 依赖不匹配,把 Servlet API 从 javax 换成 jakarta 就行。这种细节写进博客里很实用,但在代码里真正遇到时才知道有多头疼。
2. 核心模块设计与数据库规划
数据库设计是整个项目的灵魂,这一块搞定了,后面写接口只是体力活。我设计仓储系统时习惯把表分成两类:一类是“静态数据表”,比如物料表、库位表、供应商表、用户表;另一类是“作业单据表”,比如入库单、入库明细、出库单、盘点单、库存流水表。两大类交错起来,才是完整的业务流程。
2.1 物料、批次与库位:三层结构定胜负
物料的粒度决定了库存怎么算。最简单的设计是一张物料表(id, 编码, 名称, 规格, 单位, 默认库位),库存表里直接存数量。但真实仓库里,同一款物料可能有两个批次,生产日期不一样,先进先出时就必须区分。所以我建议加入批次概念,物料表只管“是什么”,批次表管“哪一批”,库存表则一定要有一个批次字段或者单独批次库存表。
库位也不用设计得太深,一般三级足够:仓库(warehouse)、库区(zone)、库位(location)。很多毕业设计只给“库位”一个字段,后面做移库和盘点时会很别扭。我通常建一张 warehouse 表,一张 location 表,location 表里有 warehouse_id 外键,这样以后想扩展多仓库也自然。
库存表可以拆成“可用库存”和“占用库存”,但为了毕设简化,我一般只用一个字段 quantity 加一个字段 locked_quantity。在出库单审核前先锁库(锁定数量),审核通过再扣减,取消单据则释放锁定,这样逻辑链完整又不难写。
2.2 入库、出库、盘点三大流程的状态机设计
每张作业单据都应该有“状态字段”,不要做完直接改库存就完事。我见很多同学把状态存成字符串 “待审核”“已审核”,然后到处硬编码,这是最容易被答辩老师追问的地方。更稳的做法是定义一个状态枚举,用数字或短代码存:
入库单状态:0 草稿、1 待审核、2 已审核、3 已驳回、4 已取消。出库单状态类似,多一个“已出库”或“已完成”。盘点单状态:0 进行中、1 待调整、2 已完成。
状态机最关键的一点:库存变化只能发生在状态从“待审核”到“已审核”那一刻。写代码时要把“查单 -> 校验状态 -> 变更库存 -> 修改单状态 -> 记录流水”放在同一个事务里,这样才能保证一致性。我在 3.3 节会讲具体事务写法。
2.3 库存流水表:不改历史,只记变化
库存流水表是我特别想强调的表。很多初学者以为有了一张库存表就万事大吉,但库存只会越改越乱,查不到历史是谁在什么时间、因为哪张单子动了库存。正确的做法是:库存表只反映当前结存,每一次数量变化都往库存流水表里追加一条记录,字段包括:流水类型(入库/出库/盘点调整/移库)、关联单据号、变动前数量、变动数量、变动后数量、操作人、操作时间。
这样设计的好处有几个:一是你可以随时追溯“为什么现在库存是这个数字”;二是做报表时直接聚合流水表,不用去猜历史数据;三是如果数据对不上,排查时能精确到哪一步出错。
2.4 数据库脚本实战与表字段设计参考
下面我直接给一个核心表的精简设计,你可以根据自己的场景调整。
物料表(material):
- id bigint
- code varchar 编码唯一
- name varchar 名称
- spec varchar 规格
- unit varchar 单位
- category_id bigint 分类id
- status tinyint 1启用 0停用
- create_time datetime
仓库与库位表(warehouse / location):
- warehouse(id, name, address, create_time)
- location(id, warehouse_id, code, area, remark, create_time)其中 code 字段就是具体的库位编号,例如 A-01-01。
库存表(stock):
- id bigint
- material_id bigint
- location_id bigint 可选
- batch_no varchar 批次号
- quantity decimal(15,3)
- locked_quantity decimal(15,3) 默认0
- update_time datetime
- unique key 通常设成(material_id, location_id, batch_no)
入库单表(inbound_order):
- id bigint
- order_no varchar 单号
- supplier_id bigint
- warehouse_id bigint
- status tinyint
- audit_user_id bigint
- audit_time datetime
- create_user_id bigint
- create_time datetime
入库明细表(inbound_order_item):
- id bigint
- inbound_order_id bigint
- material_id bigint
- batch_no varchar
- location_id bigint
- quantity decimal(15,3)
- remark varchar
出库单表与明细表结构和入库单类似:outbound_order 加一个 customer_id 或 dept_id 表示领用方,明细表里加 quantity,必要时加“出库方式/先进先出标识”。
盘点单表(stocktake_order):
- id bigint
- order_no varchar
- warehouse_id bigint
- status tinyint
- remark varchar
- create_user_id bigint
- create_time datetime
盘点明细表(stocktake_order_item):
- id bigint
- stocktake_order_id bigint
- material_id bigint
- location_id bigint
- batch_no varchar
- book_quantity 账面数量
- actual_quantity 实盘数量
- diff_quantity 差异数量
库存流水表(stock_flow):
- id bigint
- flow_type varchar 比如 INBOUND / OUTBOUND / STOCKTAKE / MOVE
- biz_no varchar 关联单据号
- material_id bigint
- location_id bigint
- batch_no varchar
- before_quantity decimal
- change_quantity decimal
- after_quantity decimal
- create_user_id bigint
- create_time datetime
建完这些表,你的系统骨架基本就立住了。接下来后端开发时,你会发现任务就是把这些表填满并且保证数据正确,没有任何模糊地带。
3. 后端落地过程与关键实现
这一章我按一个真实项目的开发顺序来写:先初始化工程,再生成代码,然后从登录、基础档案到核心单据,每个关键点都给出可操作的做法。如果你已经能建一个 HelloWorld 项目,可以直接从 3.2 节开始看。
3.1 工程初始化与项目结构分层
我习惯在建项目时直接把目录结构按“controller -> service -> mapper -> entity -> common”分好,不要到后面再补。原因很简单:毕业设计代码量不大,但至少要看起来像正规军。
标准结构如下:
com.example.warehouse ├── controller // 接收前端请求 ├── service // 业务逻辑层,写事务、状态判断 │ └── impl ├── mapper // MyBatis-Plus 的 Mapper 接口 ├── entity // 数据库实体 ├── dto // 请求参数和返回对象 ├── common // 返回结果封装、异常处理、常量 ├── config // 配置类、拦截器、权限控制 └── task // 定时任务打开 IDEA 新建项目时,我建议先选 Spring Initializr,依赖勾选 Web、MyBatis Framework(如果没有就后加 MyBatis-Plus 依赖)、MySQL Driver、Lombok,再手动往 pom.xml 里加 MyBatis-Plus 相关坐标。Lombok 必开,能用 @Data 省掉一堆 getter/setter,不然写实体类会写到怀疑人生。
说到自动装配,SpringBoot 之所以省事,是因为 spring-boot-autoconfigure 里的各种 xxxAutoConfiguration 类会根据 classpath 上的依赖帮你自动配置好数据源、MyBatis、Web MVC 等。你只要在主启动类上标注 @SpringBootApplication,它会带着 @EnableAutoConfiguration 去扫描这些配置类。理解了这一点,遇到“为什么我配了数据源但项目没连上”时,你会第一时间去检查依赖和配置文件是否被正确加载。
3.2 MyBatis-Plus 整合与基础 CRUD 生成
MyBatis-Plus 是目前写毕设项目的首选 ORM,因为它把单表 CRUD 已经全部封装好了。你只需要在 application.yml 里配好数据源,然后建一个实体类,写一个继承 BaseMapper 的接口,就能直接调用 selectPage、insert、updateById 这些方法,大部分简单查询一行代码都不用写。
我直接用“第2关:使用springboot + mybatis实现一个最简单的注册功能”那种思路来演示:假设已经建好用户表,实体类 User 标注 @TableName("sys_user"),@TableId(type = IdType.AUTO) 标注主键,Mapper 接口写成:
public interface UserMapper extends BaseMapper<User> { }Service 层我习惯再包一层:
@Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { }这样再配合 MyBatis-Plus 自带的 IService,你直接拥有了 save、updateById、list、page 等方法,非常快。第一次跑项目时,建议先在测试类里写一个最简单的入库存方法,打出一条日志,确认数据库连接没问题再继续往下写,这能帮你把“环境问题”和“业务问题”分离开。
有一点要注意:MyBatis-Plus 3.5.x 和 SpringBoot 3.x 搭配时,需要引入专门的适配包或者使用 mybatis-plus-spring-boot3-starter,否则启动会报错找不到 SqlSessionFactory。这是当前最容易踩的版本坑之一,检查时第一眼就看依赖坐标,别急着改代码。
3.3 入库单审核与库存扣减的事务控制
入库单审核是仓储系统里最有技术含量的一步,也是答辩时老师最爱问的点。这一步的核心就一句话:在同一个事务里完成状态校验、库存增加、流水记录。
我在业务代码里一般这样写:
@Transactional(rollbackFor = Exception.class) public void auditInbound(Long orderId) { // 1. 查询入库单,校验状态必须是“待审核” InboundOrder order = inboundOrderMapper.selectById(orderId); if (order == null || order.getStatus() != InboundOrderStatus.PENDING) { throw new BizException("单据不存在或状态不允许审核"); } // 2. 查询入库明细 List<InboundOrderItem> items = inboundOrderItemMapper .selectList(new LambdaQueryWrapper<InboundOrderItem>() .eq(InboundOrderItem::getInboundOrderId, orderId)); // 3. 逐条增加库存,并写流水 for (InboundOrderItem item : items) { Stock stock = stockMapper.selectOne(new LambdaQueryWrapper<Stock>() .eq(Stock::getMaterialId, item.getMaterialId()) .eq(Stock::getBatchNo, item.getBatchNo()) .eq(Stock::getLocationId, item.getLocationId())); if (stock == null) { stock = new Stock(); stock.setMaterialId(item.getMaterialId()); stock.setBatchNo(item.getBatchNo()); stock.setLocationId(item.getLocationId()); stock.setQuantity(item.getQuantity()); stockMapper.insert(stock); } else { // 注意这里用乐观锁或行锁,防止高并发时超卖 stock.setQuantity(stock.getQuantity().add(item.getQuantity())); stockMapper.updateById(stock); } // 4. 写流水 stockFlowMapper.insert(buildFlow(StockFlowType.INBOUND, order.getOrderNo(), item, stock.getQuantity())); } // 5. 更新单据状态为“已审核” order.setStatus(InboundOrderStatus.AUDITED); inboundOrderMapper.updateById(order); }这里有两个关于并发和一致性的细节:第一,如果仓库使用量不大,直接给 stock 表加一个乐观锁版本号字段,更新时用 updateById 带 version 校验;第二,更稳妥的做法是直接用数据库的UPDATE stock SET quantity = quantity + ? WHERE material_id = ? AND batch_no = ?,这种原子更新可以避免读到脏数据。我实际开发时更倾向于第二种,因为它简单且真正有效。
还有一点:事务里如果抛了异常,一定要保证前面所有更新都回滚。所以我在 Controller 里不做任何业务,只调 service,把 @Transactional 放在 service 层的方法上。同时配合统一的全局异常处理,前端拿到的就是提示信息,不会直接看到一堆异常堆栈。
3.4 定时任务与库存预警
毕业设计里加一个定时任务会显得系统很完整。我最常加的是“低库存预警”:每天检查库存表,如果某物料数量低于安全库存,就生成一条待处理预警记录,并可以在首页展示。
SpringBoot 里做定时任务非常简单,启动类上加 @EnableScheduling,然后在方法上标 @Scheduled:
@Component public class StockWarnTask { @Resource private StockService stockService; // 每天凌晨 2 点执行 @Scheduled(cron = "0 0 2 * * ?") public void checkLowStock() { stockService.generateLowStockWarnings(); } }注意 cron 表达式是六位或七位,千万别把第一位设置成周几,很容易在周一突然跑一次然后又安静一周。建议写好之后,把执行时间调成每分钟一次,观察日志确认逻辑没问题,再把 cron 改回固定时间。
如果你想在首页实时展示预警数量,最简单的做法是保存一张 warning 表,前端定时轮询统计接口;或者用 WebSocket 推送。WebSocket 在答辩时是加分项,但会增加不少代码,我一般建议把基础版本先跑通,再决定要不要加。
3.5 登录认证与权限控制
仓储系统肯定要分角色:管理员、库管员、主管、只读的财务或老板。毕业设计做“细粒度权限”会很累,我建议用 RBAC 的简化版:用户表 + 角色表 + 用户角色关联表,菜单或按钮权限暂时不用做太细,只要能控制“不同角色看到不同菜单”就已经很完整。
技术实现有三条路:
一是用 Spring Security + JWT。这是最规范的做法,但配置略多,适合你本身对 Spring Security 有一定了解。
二是写一个拦截器,自己拦截带/api/**的请求,校验 JWT token,解析出用户信息和角色,再把用户放到 ThreadLocal 或 Request 上下文中。简单直接,也不影响答辩,毕竟你能讲清楚登录流程、token 过期、拦截器原理。
三是更轻量的 Session + 拦截器。适合本地部署,但前后端分离后会有跨域和 Cookie 携带问题,不太推荐。
我实际项目里用的是 JWT 拦截器方案:
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); if (StringUtils.isBlank(token) || !JwtUtil.validate(token)) { throw new BizException("未登录或登录已过期"); } // 解析 userId,放入 request 上下文 request.setAttribute("userId", JwtUtil.getUserId(token)); return true; } }然后在 WebMvcConfigurer 里注册拦截器,并排除登录接口和静态资源。写完以后记得测一下 token 过期返回,别等到答辩演示时被老师点出“权限似乎没生效”这种尴尬问题。
4. 前端开发与打包部署
后端接口写完,才是真正能“看见”系统的时候。前端选 Vue3 + Element Plus + Vite 基本是标配。我不建议用 Vue2 了,除非你找到的模板特别好;Vue3 的组合式 API 做中后台系统反而更顺手。
4.1 Vue3 + Element Plus 后台界面设计
后台管理页面基本可以拆成四个区域:左侧菜单、顶栏用户信息、主内容区、底部或面包屑。页面之间用 Vue Router 管理,请求用 Axios 封装,状态管理用 Pinia(或者干脆不用,因为大多数页面只是表格和表单)。
页面规划大致如下:
- 首页:库存总览、预警提醒、最近入库出库记录
- 基础档案:物料管理、库位管理、供应商管理、用户管理
- 入库管理:入库单列表、创建入库单、入库审核
- 出库管理:出库单列表、创建出库单、出库审核
- 库存管理:当前库存查询、库存流水、盘点管理
- 报表统计:入库报表、出库报表(可以用简单的柱状图)
每个列表页用 Element Plus 的 el-table 加分页,表单页用 el-form 加规则校验。这套组合的好处是组件风格统一,代码量小,页面看着也专业。
说说我踩过的一个坑:el-table 的列宽和序号列处理不好,在小屏设备上一塌糊涂。最稳的办法是给第一列加type="index"显示序号,操作列固定宽度,其余列按内容设置min-width。反正毕设主要演示是在电脑上,能保证后半夜答辩时页面不崩就够了。
4.2 前后端联调与本地代理配置
后端开发时直接访问 Swagger 或 Postman 没问题,但前端要访问接口,就会遇到跨域。开发环境下最省事的方案不是在 SpringBoot 里配 @CrossOrigin,而是在 Vite 里配置代理:
// vite.config.js export default defineConfig({ server: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })这样前端请求/api/user/login会在本地被转发到http://localhost:8080/api/user/login,不会产生跨域问题。上线后你又把接口路径统一挂在同一个域名下,跨域自然也不存在,这样整个流程是干净的。
如果你想用 @CrossOrigin 临时解决跨域,注意别写@CrossOrigin(origins = "*"),配合 JWT 时会丢失 Cookie 或 Authorization 头。我建议从一开始就按代理的方式走,这更贴近真实工程。
4.3 把 Vue 打包放进 SpringBoot 中
很多同学问“vue打包放进springboot中”该怎么操作。原理很简单:Vue 执行npm run build后,dist 目录下生成了 index.html、js、css 等静态文件;SpringBoot 的静态资源默认放在src/main/resources/static下,你把 dist 里的内容复制进去,项目启动后直接访问http://localhost:8080/就能看到前端页面。
但要注意几个细节:
一是 Vite 打包时如果设置了base: '/',那所有 js/css 链接都是绝对路径,如果你的项目部署在根路径下没问题;如果以后想放到/warehouse/子路径下,就必须改 base。毕设一般都放根路径,不用纠结。
二是路由模式最好设成createWebHashHistory(),也就是哈希路由。这样可以避免刷新页面时出现 404,因为后台没有配置 forward 到 index.html。
三是如果想更系统地管理,可以借助 Maven 的 resources 插件,在打包时把前端 dist 自动复制到静态资源目录。有 CI/CD 意识是加分项,没有的话手动复制也能跑。
4.4 部署到云服务器与 Docker
毕设演示时,本地跑个 jar 包也可以,但如果老师让你“远程访问”或者你交了项目后环境变了,部署到服务器会更稳。我通常推荐两种部署方式:
一种是传统部署:服务器装好 MySQL、JDK,上传 jar 包,运行java -jar warehouse-0.0.1.jar,再把前端静态资源放到 jar 包内或者用 Nginx 服务。这种方式排查问题直观,适合新手。
另一种是 Docker Compose 部署:把 MySQL 和 SpringBoot 分别做成容器,用 docker-compose.yml 编排。虽然要写 Dockerfile 和配置文件,但可复现性很好,换一台服务器也能一键起服务。如果你前面用了“宝塔”,也可以在宝塔的 Docker 管理器里直接添加容器,外网映射一下端口就能访问。
这里有一个必注意的事项:MySQL 容器里如果没做数据持久化,重启容器你的入库单数据就全没了。所以一定要给 MySQL 挂载 volume,或者直接不走容器化数据库,用云服务器自带的 MySQL 服务,项目 jar 包容器化,这样数据安全性高一点,也好解释。
5. 常见问题与排查技巧实录
写到最后,我把这几年带项目时遇到的高频问题整理成一个速查表。这些问题看起来散,但每一个都可能让你从“自信满满”变成“怀疑人生”,提前知道怎么排查会省很多时间。
5.1 SpringBoot 版本太高引发的兼容问题
前面提过的新建项目默认版本过高,最常见的现象是 MyBatis-Plus 启动报错、javax.servlet 找不到、Lombok 版本不兼容等。我的处理顺序是:先去 pom.xml 里看 SpringBoot 父版本,再对照 MyBatis-Plus 官方文档找对应的 starter 版本;如果懒,直接降到 SpringBoot 2.7 + JDK 8 复现网上的教程,通常一次性通过。
还有一件事:IDEA 里如果 2026 版(热词里居然有人提到)新建项目默认给了最新框架,记得检查编译与运行配置,Java SDK 别选错。SpringBoot 3.x 至少要求 JDK 17,你如果机器装的是 JDK 8,项目源码编译阶段就过不了。
5.2 数据库连接失败与时区问题
SpringBoot 项目连 MySQL 时报Access denied for user,大概率是密码错了或者用户没权限。还有一个高频问题是The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,这是因为 MySQL 驱动 8.x 对时区校验严格。解决方案是在 JDBC URL 后加参数:
jdbc:mysql://localhost:3306/warehouse?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai写完这个基本就不会再报时区错。如果在 Docker 里连不上,先检查容器网络和端口映射,不要一上来就怀疑代码。
5.3 高并发扣库存导致数据不一致
毕设项目一般没什么并发量,但答辩时可能会被问“如果两个人同时下单,库存会不会变负?”这是经典问题。如果你只是用了 select 后 updateById,那在高并发下一定出问题。解决办法前面说过:用数据库的原子更新,或者给库存表加乐观锁版本号。
int rows = stockMapper.deductStock(materialId, batchNo, quantity); if (rows == 0) { throw new BizException("库存不足或记录不存在"); }对应的 XML 就是:
UPDATE stock SET quantity = quantity - #{quantity} WHERE material_id = #{materialId} AND batch_no = #{batchNo} AND quantity >= #{quantity}面试官听到你能说出“原子更新和乐观锁”,基本上就不会揪着并发问题继续追问了。
5.4 前后端联调的跨域、路径与刷新 404
前端请求 404,优先看接口前缀和后端 controller 的 @RequestMapping 是否一致;前端页面刷新 404,检查路由模式是不是 hash;请求发出去了但响应是 401,检查 JWT 是否过期、请求头里是否有 Authorization。
推荐一个调试技巧:打开浏览器开发者工具,Network 面板里看请求响应状态,Console 里看 Vue 和 Axios 的错误拦截输出。大部分联调问题不是代码逻辑,而是路径大小写、参数名拼写、JSON 字段命名不一致造成的。把接口文档或者 Swagger 的字段 copy 出来逐一对照,效率最高。
5.5 答辩与面试中的高频问题速查表
老师或面试官如果对这个项目有兴趣,问题通常集中在几个点上:
| 问题 | 你该怎么答 |
|---|---|
| 为什么用 SpringBoot? | 简化配置、自动装配、内嵌服务器、生态成熟 |
| 库存数据怎么保证一致性? | 事务 + 原子更新/乐观锁 + 流水记录 |
| 说说你项目的角色权限? | 用户-角色-权限三张表,后端拦截器做 JWT 校验 |
| 先进先出怎么实现? | 出库明细按批次时间排序,先扣最早批次 |
| 如果有多个仓库怎么扩展? | 库存表已带 warehouse_id,location 也属于某个仓库 |
| 为什么要写库存流水表? | 审计需要、追溯需要、报表数据来源 |
| 项目里最困难的部分? | 入库审核时的并发扣库存,用原子更新解决 |
| 定时任务怎么做的? | @EnableScheduling + @Scheduled,配合业务表做预警 |
这些问题的答案都不需要很深,但你要能用自己的话讲出来,别背概念。
最后再分享一个我自己的习惯:写这类毕业设计项目时,每写完一个功能,就顺手把测试数据填一遍,把“正常流程”和“异常流程”都走通。比如入库单审核前库存不变、驳回后还能重新提交、盘点差异后库存被调整,这些细节跑步通,答辩时一旦演示到一半卡住,你会非常被动。把核心链路(建物料 -> 建库位 -> 入库 -> 审核 -> 出库 -> 审核 -> 查库存 -> 看流水)反复跑三到五遍,做到闭着眼都能演示,这比任何代码注释都有说服力。
这个项目后续如果想再加深,可以把条形码扫描场景做成移动端页面,也可以加一个简单的报表可视化大屏,甚至可以把采购单和销售单串进来形成完整供应链链路。但基础版的这套 SpringBoot 仓储管理系统已经足够让你把业务建模、工程结构、事务处理和部署发布都讲明白了,先把它做扎实再说。