ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java物资管理系统源码实战:Spring Boot+MyBatis-Plus+MySQL完整拆解

Java物资管理系统源码实战:Spring Boot+MyBatis-Plus+MySQL完整拆解 简介这套Java物资管理系统源码是一份面向Java学习者、毕业设计选题者及仓储管理开发者的完整项目包。系统覆盖商品、供应商、采购、入库、出库等核心业务模块基于Java SE、JDBC与MySQL数据库构建界面注重易用性并集成了用户身份验证、数据备份等安全机制各模块间通过数据联动形成完整管理闭环。压缩包内共212个文件大小约11.7MB主要包含51个jsp页面用于前端交互展示、36个java源文件封装业务逻辑、36个class编译文件、30个xml配置、24个jar依赖库以及部分图片、脚本与项目配置文件借助Maven构建可便捷部署。资源还附带了详尽的文档和使用手册并采用了以DAO类划分数据访问层的清晰目录结构方便读者从环境配置、数据库表设计到业务代码实现逐层研读。目前资源已有319人学习下载既能作为企业物资管理的实用方案也可作为深入学习Java与数据库开发的优秀案例整体实用性与参考价值较高。1. 物资管理系统Java源码它不是玩具项目是去小公司面试前必须跑通的那类业务一个物资管理系统听着就是增删改查但你真把源码拿到手里会发现它比想象中麻烦库存要对上账出入库要有流水删一个物资得先问有没有单据引用它。Java 写的这类项目源码满天飞但我更愿意跟你说它值得你花一个周末认真跑一遍。它能让你在面试时讲清楚“从建表到 Controller 的闭环”也能让你在小团队真正接手运维时少翻车。这篇就按最常见的 Spring Boot MyBatis-Plus MySQL 方案从项目结构、建表、核心代码到踩坑点老老实实拆给你照着做就能跑通。2. 先拆需求再碰代码物资管理系统的核心功能与典型数据模型2.1 六张表撑起一个进销存从物资台账到出入库流水我见过不少网上的物资管理系统源码表结构五花八门。但把业务抽象完核心逃不出这几张表物资信息表存物资名称、规格、单位、库存量、物资分类表、供应商表、入库单表、入库明细表、出库单表、出库明细表。有人说这都八张了怎么还叫六张。其实分类表和供应商表是辅助主表真正撑起进销存闭环的是物资主表、入库单、入库明细、出库单、出库明细再算上一个用户表把鉴权放进去日常就够用了。为了好记我一般称它为核心六表加两张辅助表。设计思路是主表放物资的基础属性和当前库存明细表放每次出入库的具体条目单表放一次业务操作的抬头单号、时间、经办人、备注。为什么要把单和明细拆开因为一条入库单可能包含多种物资每种物资的数量、单价、批次可能不同。如果全部塞进一张表你要么每行重复单号要么只存一个物资业务根本没法扩。拆开之后主表只保存最终库存量明细表就是审计流水任何一次出入库的历史都能追溯。这个模型也是面试里常聊的“表设计范式”的落地例子。下面这段 SQL 是我在项目里常用的核心建表脚本。注意字段类型和索引的位置。CREATE TABLE t_material ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键, code varchar(32) NOT NULL COMMENT 物资编码, name varchar(64) NOT NULL COMMENT 物资名称, spec varchar(128) DEFAULT NULL COMMENT 规格型号, unit varchar(16) DEFAULT NULL COMMENT 计量单位, category_id bigint(20) DEFAULT NULL COMMENT 分类ID, stock int(11) NOT NULL DEFAULT 0 COMMENT 当前库存, min_stock int(11) DEFAULT 0 COMMENT 库存预警下限, status tinyint(4) DEFAULT 1 COMMENT 状态1启用 0停用, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_code (code), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT物资信息表;这里把库存预警下限 min_stock 直接放在主表给后面做库存预警留着钩子。物资编码 code 加了唯一索引因为手工录入时最容易重复让数据库兜底。status 字段用于软状态不要把“停用”和“删除”混为一谈。如果你拿到的源码里没有 min_stock说明它的预警功能是后加的你要做库存预警时得自己补上。接着看入库与出库。这两张表的明细结构几乎一样只差方向。入库拆成 t_stock_in 和 t_stock_in_item出库是 t_stock_out 和 t_stock_out_item。明细表必须带上单价、数量、金额并且外键指向单表。我用一笔入库单举例。CREATE TABLE t_stock_in ( id bigint(20) NOT NULL AUTO_INCREMENT, in_no varchar(32) NOT NULL COMMENT 入库单号, supplier_id bigint(20) DEFAULT NULL, operator varchar(32) DEFAULT NULL COMMENT 经办人, in_time datetime DEFAULT NULL COMMENT 入库时间, remark varchar(255) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_in_no (in_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_stock_in_item ( id bigint(20) NOT NULL AUTO_INCREMENT, in_id bigint(20) NOT NULL COMMENT 入库单ID, material_id bigint(20) NOT NULL, quantity int(11) NOT NULL COMMENT 入库数量, price decimal(10,2) DEFAULT NULL COMMENT 进货单价, amount decimal(12,2) GENERATED ALWAYS AS (quantity * price) STORED COMMENT 金额, PRIMARY KEY (id), KEY idx_in_id (in_id), KEY idx_material (material_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意这里金额字段我用的是 MySQL 的生成列让数据库自己算 quantity 乘 price避免 Java 代码里漏算或算错。这个用法很多源码里不会写你敢写出来面试官会觉得你懂数据库特性也确实是 java 面试题里对账精度问题的反例之一。更重要的是入库操作里中途任何一条明细插入失败整个事务要回滚单号和明细不能对不上所以外键 in_id 指向主表 id。同样出库表也按这套结构复制一份只是把 in 换成 out。2.2 为什么用Spring Boot MyBatis-Plus 而不是SSH很多老一点的项目源码还在用 SSMSpring SpringMVC MyBatis甚至 SSHStruts Hibernate。你要跑通它们得配一堆 XML启动一个 Tomcat 要半分钟JDK 版本还不能乱动。而 Spring Boot 把配置收敛到 application.yml内嵌 Tomcat一键启动。对于要快速看效果、改代码、应付面试的场景Boot 是当下最现实的选择。MyBatis-Plus 的意义在减少重复的 CRUD 代码。它内置了通用 Mapper 方法比如 selectById、selectList、insert 等你不用像原生 MyBatis 那样一个实体类对应一个 XML 文件写四个基础标签。但 MyBatis-Plus 也不是银弹复杂联表查询你仍然要手写 SQL它的 QueryWrapper 写多表 join 反而绕。所以我的习惯是单表操作交给 Wrapper多表简单关联用注解 Select复杂报表才上 XML。这里面的取舍面试时也常被问到属于 java 八股文里的高频题只不过很多人只背结论没看过真实业务。为什么不选更花哨的微服务有人一上来就推 Spring Cloud Alibaba、Redis、MQ。我劝你不要在一个单体源码上叠加这些东西。物资管理系统本质是单库、低并发、重数据一致性。用微服务拆只会把事务搞成分布式事务难度翻倍。我接手过一个小团队的项目他们把库存扣减放到了 MQ 异步里结果服务器重启丢了一条消息库存就不准了最后还是改回 Spring Boot 单体事务放在 Service 层让数据库保证一致性。稳定的方案往往不性感但它让你睡得着觉。2.3 建表SQL该怎么写字段类型与索引的取舍这一节专门讲新手最容易踩的建表坑一共四条。第一个坑库存字段用 int 还是 decimal。库存量是数量不是金额用 int 就够了。如果原料按重量算比如 kg考虑 decimal(12,3)。千万不要把库存写成 float 或 double浮点数的精度问题会让你对账对不上。金额字段统一用 decimal禁止用 float这也是前面建表脚本里单价和金额都用 decimal 的原因。第二个坑decimal 的精度。单价 decimal(10,2)金额 decimal(12,2)如果你的单笔金额超过 99 亿那是业务问题了一般不用管。但数量乘单价的结果可能超过单价本身的精度所以金额字段的 precision 要比单价宽两到三位否则插入时报 out of range你半天查不出是数字溢出的问题。第三个坑索引不要全加。很多源码里每个字段都加 KEY纯属凑热闹。物资表只需要主键、唯一编码、分类索引明细表的 in_id 和 material_id 各加一个索引单号加唯一索引。像 status、remark 这种字段加了索引也提高不了查询反而拖慢插入。索引数量越多写入越慢这是数据库的常识。第四个坑字符集统一用 utf8mb4不要用 utf8。utf8 在 MySQL 里是 3 字节存 emoji 或生僻字会报错。现在 MySQL 8 默认就是 utf8mb4但老项目可能还带着 latin1。拿到源码后第一步查看所有表字符集不一致的后果是 join 时索引失效慢查询让你怀疑配置是不是写错了。字段命名上也值得统一表字段用下划线分隔Java 属性用驼峰MyBatis-Plus 默认开启驼峰映射所以不需要额外配置。但如果你自定义了某个字段的映射一定要在实体类上用 TableField 显式标注防止改字段名时漏改。最后说初始化源码根目录通常有 doc/sql/init.sql或者 sql/data.sql。如果找不到你就得自己用上面这些建表语句把库建出来。建库时建议把字符集一次指定好后面再改很麻烦。执行完建表脚本后可以用 SHOW CREATE TABLE t_material 核对字段和索引是否符合预期这一步花不了两分钟但能让你在后面对代码时不心虚。3. 把源码跑起来从JDK、MySQL到配置文件的最小启动路径3.1 环境准备JDK版本、MySQL8、Maven的坑拿到一份 Java 的物资管理系统源码第一步不是急着双击运行而是先打开 pom.xml 看 java.version。有的写 1.8有的写 11有的写 17。如果你本机 JDK 是 21大概率会编译报错或者运行时报非法反射。我一般会装一个 JDK 8 和一个 JDK 11用 IDEA 的 Project Structure 切换哪个项目就用哪个版本别让 IDEA 自动下 JDK那样经常下到不匹配的发行版。MySQL 我建议直接装 8.0 以上因为很多新源码把 driver 写成了 com.mysql.cj.jdbc.Driver如果你还装 5.7驱动类不对启动就报 ClassNotFoundException。另外 MySQL 8 默认认证插件是 caching_sha2_password老版本的 mysql-connector-java 会连不上。所以不要自己随便升级驱动版本源码 pom 里写了哪个版本就用哪个除非你知道自己在做什么。Maven 那边注意 settings.xml 的镜像。国内用默认中央仓库下载依赖会等到怀疑人生。我一般会在本地配置阿里云镜像这是血泪经验。不需要问作者要仓库地址只要在 ~/.m2/settings.xml 里加一个 mirror 指向阿里。配置完以后IDEA 里执行 mvn clean compile能顺利下载完依赖项目才谈得上“能跑”。3.2 导入源码到IDEA后第一件事改配置文件不要一上来就点运行。先打开 src/main/resources/application.yml 或 application.properties把数据库连接改成你的本地库。常见配置如下spring: datasource: url: jdbc:mysql://localhost:3306/material_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里每一行都有讲究。url 里必须有 serverTimezoneAsia/Shanghai否则你查出的时间会比本地早八小时前端列表显示的时间全是错的。useSSLfalse 防止 MySQL8 连接时的 SSL 告警。allowPublicKeyRetrievaltrue 是 caching_sha2_password 情况下必要的参数不加会报 Public Key Retrieval is not allowed很多人第一次启动就是这个错。db-config 里配置了逻辑删除字段 deleted表示源码里用了逻辑删除设计你删除物资时实际上是置 deleted1而不是物理删行。这一点会被很多人忽略导致直接对数据库改 deleted 字段物理删数据。还要检查 application.yml 里的 server.port。默认 8080如果你本机已经跑了一个占用 8080 的应用会启动报端口被占用。我习惯把它改成 8081和已有的 Java 服务错开省得每次启动都去查端口。3.3 启动命令与验证入口前端页面、Swagger、接口文档启动有两种方式。IDEA 里直接运行主类主类一般叫 MaterialApplication、Application 或 XxxApplication类名上标着 SpringBootApplication。另一种是命令行用 Maven 插件运行mvn spring-boot:run如果你想打成 jar 再跑先清理后打包mvn clean package -DskipTests java -jar target/material-0.0.1-SNAPSHOT.jar跑起来后看到日志里出现 Tomcat started on port 8080说明启动成功。此时先不要操作页面先验证接口。很多源码带 Swagger 配置启动后访问 /swagger-ui.html 或 /doc.html 能看到全部接口列表。如果没有 Swagger就用浏览器直接访问前端登录页通常是 http://localhost:8080/index.html 或 /login。我验证的顺序是先看登录接口POST 登录拿到 token然后带 token 查物资列表接口。为什么先验证列表因为列表接口会走完整的 Controller、Service、Mapper、DB 链路这一条通说明数据库连接、MyBatis 映射、参数封装基本没问题。很多新人一上来就点入库接口结果中间好几个字段名对不上排查起来全是干扰。可以用 curl 快速验证curl -X POST http://localhost:8080/api/login \ -H Content-Type: application/json \ -d {username:admin,password:admin123} curl -X GET http://localhost:8080/api/material/list \ -H Authorization: Bearer token有些源码不使用 JWT而是用 session那 Authorization 头就不需要。你看代码里的拦截器就知道。如果前端页面能打开但接口 404先看一眼 resources/static 里有没有 index.html。有说明前后端一体打包在 Spring Boot 里没有说明是前后端分离项目你还得自己去跑前端的 npm 服务。最后一次叮嘱启动失败不要盯着满屏红字慌控制台里真正有用的是最后一行 Caused by。数据库连接、端口、驱动这三个原因的报错全部从 Caused by 开始看。把根因解决了上面的异常堆栈自动就消失了。4. 核心模块代码复现物资入库与出库的业务闭环4.1 实体类设计TableName与TableId加一个字段就要同步改三处实体类是 MyBatis-Plus 映射表结构的关键。用 Lombok 简化 getter/setter。以下是物资实体的典型写法Data TableName(t_material) public class Material { TableId(type IdType.AUTO) private Long id; private String code; private String name; private String spec; private String unit; TableField(category_id) private Long categoryId; private Integer stock; TableField(min_stock) private Integer minStock; private Integer status; TableField(create_time) private LocalDateTime createTime; TableField(update_time) private LocalDateTime updateTime; }注意 TableField(category_id) 这种写法把 Java 的驼峰属性映射到下划线字段。如果你不写MyBatis-Plus 默认也开启驼峰映射categoryId 能映射到 category_id。但显式声明的好处是防止你修改字段名时漏掉映射。我改表结构时遵循“三处同步”原则改表字段时同时改实体类、改 Mapper XML如果有手写 SQL、改前端传入的参数对象DTO。少一次运行时不报错但数据错位这种错位是最可怕的因为它不会给你异常提示。如果你想让 MyBatis-Plus 根据实体类生成建表 SQL也可以用类上的字段注解去拼接 DDL比如把 TableName 当表名、TableField 当列名。但大多数源码还是手写 SQL 更可控因为建表语句里的索引、注释、默认值在注解里表达不完整。4.2 Mapper层用MyBatis-Plus的Wrapper写动态查询Mapper 接口直接用 BaseMapper 扩展Mapper public interface MaterialMapper extends BaseMapperMaterial { }然后列表查询可以这样写LambdaQueryWrapperMaterial wrapper Wrappers.lambdaQuery(); wrapper.like(StringUtils.hasText(name), Material::getName, name); wrapper.eq(status ! null, Material::getStatus, status); wrapper.orderByDesc(Material::getCreateTime); ListMaterial list materialMapper.selectList(wrapper);这里的条件判断 like(condition, ...) 和 eq(condition, ...) 很关键如果前端没传 name条件字符串为空就不拼接这个过滤条件。很多人习惯手动 if 拼 SQL条件一多就容易漏导致全表数据被查出来。用 Wrapper 的 condition 重载是 MyBatis-Plus 最推荐的方式代码干净还不容易漏条件。但如果要统计每种物资的总出入库量单表查询就搞不定了需要在 Mapper 里手写 SQL。例如Select(SELECT m.name, m.code, IFNULL((SELECT SUM(quantity) FROM t_stock_in_item si WHERE si.material_id m.id), 0) AS total_in, IFNULL((SELECT SUM(quantity) FROM t_stock_out_item so WHERE so.material_id m.id), 0) AS total_out FROM t_material m WHERE m.status 1 ORDER BY m.id) ListMaterialStatVO selectMaterialStat();这种子查询写法比 join 更直白适合数据量几万条以内的场景。如果数据量大了再考虑物化视图或聚合表但在一个小型物资系统里这个 SQL 跑起来完全没有压力。4.3 Service层事务注解到底该放在哪物资出库这个动作至少要更新两个地方写入出库明细、扣减主表库存。这两个操作必须在一个事务里。如果你分别调用两个 mapper 方法中间只要有任何一段抛异常库存就会和流水对不上。这是网上很多垃圾源码的翻车点也是 java 面试题里“事务注解失效场景”的经典素材。正确做法是Service public class StockOutService { Autowired private StockOutMapper stockOutMapper; Autowired private StockOutItemMapper stockOutItemMapper; Autowired private MaterialMapper materialMapper; Transactional(rollbackFor Exception.class) public void doStockOut(StockOutDTO dto) { // 生成出库主单 StockOut out new StockOut(); out.setOutNo(generateOutNo()); out.setOperator(dto.getOperator()); out.setOutTime(LocalDateTime.now()); stockOutMapper.insert(out); // 写入明细并扣减库存 for (StockOutItemDTO itemDto : dto.getItems()) { StockOutItem item new StockOutItem(); item.setOutId(out.getId()); item.setMaterialId(itemDto.getMaterialId()); item.setQuantity(itemDto.getQuantity()); stockOutItemMapper.insert(item); // 扣减库存前先做业务校验 Material material materialMapper.selectById(itemDto.getMaterialId()); if (material null) { throw new BusinessException(物资不存在 itemDto.getMaterialId()); } if (material.getStock() itemDto.getQuantity()) { throw new BusinessException(库存不足 material.getName()); } // 用原子更新语句扣减不要用 select update 组装 materialMapper.reduceStock(itemDto.getMaterialId(), itemDto.getQuantity()); } } }事务注解写在 public 方法上。要注意如果这个方法被同类内部的私有方法调用事务会失效因为 Spring 的代理只在外部调用时才生效。另外rollbackFor Exception.class 必须写否则默认只在运行时异常时回滚。如果 BusinessException 继承了 Exception受检异常而不是 RuntimeException你写注解不指定 rollbackFor就会眼睁睁看着第一个 insert 成功第二个失败却不回滚。血的教训。扣减库存这里不能用先 select 再 set 的方式。两个并发请求同时读到库存 10各自准备扣 8最后 update 会把库存写成 2正确结果应该是 2 吗不对两个请求各扣 8库存 10 不可能同时满足但第二个请求的商品已经插入成功了库存却变成 2数据全乱了。所以要用原子 SQLUpdate(UPDATE t_material SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity}) int reduceStock(Param(id) Long id, Param(quantity) Integer quantity);执行后返回影响行数为 0说明库存不够或物资不存在。这种判断比 select 后再判断更可靠也能防止超卖。4.4 Controller层参数校验与统一返回体Controller 层要轻。很多源码把业务逻辑和参数转换全堆在 Controller 里一个方法两百行。轻量的 Controller 该做什么收参数、做简单校验、调用 Service、包装返回体。RestController RequestMapping(/api/material) public class MaterialController { Autowired private MaterialService materialService; Autowired private StockOutService stockOutService; PostMapping(/list) public RListMaterial list(RequestBody(required false) MaterialQueryVO query) { if (query null) { query new MaterialQueryVO(); } return R.ok(materialService.queryList(query)); } PostMapping(/stock-out) public RVoid stockOut(RequestBody Valid StockOutDTO dto) { stockOutService.doStockOut(dto); return R.ok(); } }统一返回体 R 一般包含 code、message、data 三个字段。不要让 Controller 直接返回实体类那样前端没法统一处理错误状态。这里用 Valid在 StockOutDTO 的 quantity 字段上加 NotNull 和 Min(1) 注解Spring Boot 校验失败会自动抛 MethodArgumentNotValidException。你再加一个全局异常处理器就能把这些异常转成业务提示而不是默认的 500 页面。这套代码思路同样适合改造旧源码如果拿到手的项目没有事务、没有统一返回体、直接返回 Map你要做的就是把这些坏味道一点点收拢。别想着推倒重来先把出库和入库两个接口改对再逐步扩展这样风险最小。5. 避坑与常见问题排查新人跑物资管理系统翻车的五个典型场景5.1 现象数据库连接失败报“Unknown database material_db”这个最常见。你配置了 jdbc:mysql://localhost:3306/material_db但本地 MySQL 里根本没有这个库。我见过不少人到了这一步没有检查库是否存在直接去改代码越改越乱。原因就是源码里的建表脚本没有执行或者你建库的名称和配置不一致。解决起来很简单先用命令行创建库再把源码提供的 SQL 文件灌进去。mysql -u root -p -e CREATE DATABASE IF NOT EXISTS material_db DEFAULT CHARACTER SET utf8mb4; mysql -u root -p material_db doc/sql/init.sql如果源码没有 init.sql你就按第二章的建表脚本自己执行。执行完可以用 SHOW TABLES 确认表是否创建成功。这一步是后续所有操作的起点没建库后面全是没有选择数据库的报错。5.2 现象Tomcat启动时端口被占用报错类似 Port 8080 was already in use。原因很简单本机有另一个进程占用了 8080。解决方法是定位并释放端口或者改 Spring Boot 的 server.port。我建议优先改端口因为开发机上经常同时跑多个 Java 服务杀掉别的项目反而误事。定位端口的命令Linux/Mac 用 lsof -i:8080Windows 用 netstat -ano | findstr 8080拿到 PID 后再 kill 或 taskkill。如果是在 IDEA 里重新启动注意上一次的进程可能没被完全停止IDEA 会提示端口占用直接点 Stop 再启动。有个玄学问题是改了端口后访问页面还是 8080那是因为前端代码里把请求路径写死了你要去前端的 js 配置文件里找 baseUrl一并改成新端口。5.3 现象MyBatis-Plus查询返回的字段是 null但数据库里有值这个坑很多新手都会遇到查询列表时某个字段明明有值返回对象里却是 null。原因通常是实体类字段名和表字段对不上比如表字段是 stock_qty实体类字段叫 stock。MyBatis-Plus 的驼峰映射只能把 stockQty 映射到 stock_qty如果实体类字段名不是 stockQty自然找不到列。解决方法是检查实体类所有字段用 TableField 显式标注不一致的列名或者统一修改一边。更隐蔽的情况是表里有两个字段比如 spec 和 specification实体类只写了一个另一个直接被忽略。这种问题不会报错是静默丢字段所以拿到源码后要对着建表语句逐个核对实体类字段。有时候 IDEA 打开实体类你会看到有一堆字段没有 TableField但能查出来说明驼峰映射兜底了一旦你改了字段名兜底就失效了别抱怨框架先看字段对没对齐。5.4 现象删除物资时报外键约束失败报错 Cannot delete or update a parent row: a foreign key constraint fails。你删的是 t_material 里的记录但出库明细、入库明细里还有 material_id 引用了它。这是数据库在保护你防止业务账对不上。你如果强行删就会留下孤儿明细后续统计全错。解决方式看业务如果物资已经发生过出入库历史单据需要保留那这个物资就不能物理删应该用逻辑删除status 置为停用或 deleted 置 1。如果确实要清理测试数据先清明细再删主表DELETE FROM t_stock_out_item WHERE material_id 1; DELETE FROM t_stock_in_item WHERE material_id 1; DELETE FROM t_material WHERE id 1;注意顺序先删子表再删主表。在真实项目里我建议把逻辑删除作为默认策略物理删除只留给 DBA 操作权限控制好免得手滑删了一整条链路的数据。5.5 现象前端页面能打开但接口请求404或401404 和 401 要分开看。404 通常是前端页面和后端接口不在同一个服务上前端由 Nginx 或 Webpack 提供后端接口在 8080两者端口不同前端请求后端时跨域后端没开 CORS请求就被拦了。解决方法是加一个全局 CORS 配置。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:5173) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true); } }注意 allowedOrigins 写成你前端实际地址allowCredentials(true) 时不能用 *。如果是前后端一体部署Nginx 配置里把 /api 反向代理到后端端口也可以解决同样的跨域问题。401 的常见原因有两个一是登录后 token 没保存二是请求头没带 Authorization。先看前端的登录逻辑确认 token 存到了 sessionStorage 或 localStorage然后看 axios 或 fetch 的拦截器里有没有把 token 塞进请求头。很多源码内置了 JWT 拦截器没带 token 一律返回 401这属于正常保护不是 bug。6. 在源码上做进阶给物资管理系统加一个库存预警验证你的改动可控6.1 需求定义与表结构改动新增预警逻辑不破坏原代码前面设计时主表已经预留了 min_stock 字段现在加一个定时任务每天扫描一遍 stock min_stock 的物资记录预警日志。这个功能技术要求不高但能检验你对表结构、MyBatis-Plus 条件构造和定时任务的理解。改动最小不碰原有的出入库接口风险可控适合作为源码的第一次扩展。6.2 核心实现一个Scheduled定时任务 一个查询接口实现只需要在 Service 层加一个方法。下面这段代码可以直接放进项目的任意 Service 或 Component 类里Component Slf4j public class StockWarnTask { Autowired private MaterialMapper materialMapper; Scheduled(cron 0 0 2 * * ?) public void scan() { ListMaterial warnList materialMapper.selectList( Wrappers.MateriallambdaQuery() .le(Material::getStock, Material::getMinStock) .eq(Material::getStatus, 1) ); if (warnList.isEmpty()) { return; } warnList.forEach(m - log.warn(库存预警{} 当前库存{} 低于下限{}, m.getName(), m.getStock(), m.getMinStock())); } }注意 Scheduled 需要启用定时任务你需要在某个配置类上加 EnableScheduling。然后可以通过修改 cron 表达式来调度比如 0 0 2 * * ? 表示每天凌晨两点。为了验证你可以临时改成 0/10 * * * * ? 每十秒跑一次看到日志输出了再改回来。别把测试频率留在生产上。6.3 验证用单元测试锁定改动别让项目变成黑匣子我给这种功能写过测试核心逻辑是构造数据调用 scan看是否符合预期。用 Mockito 可以模拟 mapper更简单的做法是准备一个测试库插入一条库存为 0、min_stock 为 1 的物资然后执行 scan断言日志或结果集里有这条记录。这样你后续改其他代码跑一遍测试就知道有没有影响库存预警。我的习惯是改之前先跑一遍项目原有的测试确认绿改完后跑全部测试确认新增测试绿旧测试也绿。如果旧测试挂了就是你的改动碰了不该碰的东西赶紧排查。这套“小步验证”的习惯让我在这个项目上少踩了很多坑。库存预警功能本身不复杂但它像一块试金石能看出你真正理解了代码之间的边界。如果你手头也有一份 Java 物资管理系统源码我建议你从它入手把它跑通、改通、测通而不是收藏在硬盘里落灰。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进