ARTICLE DETAIL

资讯详情

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

Java毕设实战:物资管理系统从数据库设计到核心代码避坑全解析

Java毕设实战:物资管理系统从数据库设计到核心代码避坑全解析 简介一套Java毕业设计“物资管理系统”完整资料包面向计算机专业毕业生及需要快速搭建毕设项目的开发者集合源代码、数据库脚本、毕业论文与答辩PPT覆盖从系统开发到论文答辩的关键环节。资源共10个文件主要包含jpg界面截图、zip源码与论文压缩包、sql数据库脚本以及在线演示视频链接压缩包整体约8.78MB截图清晰展示了系统登录、功能结构、客户列表等核心页面数据库脚本内含建表语句和初始数据导入即可正常运行包内资料按代码、论文、截图、数据库及视频链接分类存放便于快速定位与查阅。目前已有1431人学习。系统围绕客户管理、租赁管理、财务管理、建材管理等模块展开业务逻辑完整可直接作为课程设计或毕业设计模板论文部分涵盖开题、需求分析、系统设计与数据库设计等完整章节。对照论文理解设计思路结合源码掌握具体实现借助演示视频完成部署答辩PPT稍作调整即可用于正式答辩能显著提升毕业设计完成效率尤其适合毕业设计季快速上手。1. 物资管理系统毕设为什么这个选题能让你省下两个月选毕业设计题目时物资管理系统是被问到最多的一个它既有清晰的基础档案又有每天发生的出入库流水月底还要看统计报表难度刚好卡在“能讲清楚”和“有东西可写”之间。很多同学拿到的是一个压缩包里面装着论文、答辩PPT、源代码和数据库脚本但打开之后最常遇到的问题不是跑不起来而是不知道这些文件之间怎么对应、改了哪段代码会牵动哪张表。这篇笔记就按我梳理一套 Java 毕设项目的顺序把选题拆解、数据库设计、核心代码、常见翻车点和答辩前的验证方法一次讲透。适合准备用 Java 做毕设、又想把每一行代码和论文结论对得上的人不管你现在拿到的是自己写的代码还是别人打包的资源照着这个顺序都能理清。2. 系统拆解与技术选型先想清楚模块、表和 Java 技术栈再动手写代码2.1 功能模块怎么切基础信息、库存、出入库、统计报表四块不能少不要一上来就建表写代码先把业务模块在白板上过一遍。物资管理系统的业务闭环是“采购入库—领用出库—库存盘点”围绕这个闭环拆出四个模块模块数量刚好能支撑起论文里的功能设计章节又不会像电商系统那样失控。模块核心页面业务作用基础信息管理物资分类、物资档案、供应商管理给后续单据提供下拉选项数据源头库存管理库存查询、库存预警、盘点单回答“现在仓库里有什么、够不够”出入库管理入库单、出库单、单据明细回答“物资是怎么进来的、怎么出去的”统计报表月度入库、月度出库、库存趋势回答“这个月业务量怎么样”基础信息模块里为什么要单独放供应商很多同学把供应商塞在入库单里一个文本框就完事了这样做论文里就没有“供应商管理”这个功能点可写。把供应商抽成一张表入库单通过外键关联它系统才具备最基本的采购溯源能力同时也让数据库设计多出一对多关系可以描述。出库单这边对应的是“领用人”可以放一个部门或者领用人员字段在明细表里同样是为了让业务流程有头有尾。库存模块里有个容易被忽略的“预警”功能。我在物资表上加了两个字段最低库存量min_stock和最高库存量max_stock。库存低于最低值提示补货高于最高值提示积压这两个字段在查询列表和首页看板里各用一次实现成本极低但论文功能描述和答辩PPT里都能多一张截图。统计报表模块建议用 ECharts 画柱状图和折线图后端接口只需要返回“月份数值”的 JSON 数组前端代码网上到处都有属于性价比很高的加分项。2.2 技术栈选型Spring Boot MyBatis-Plus 是目前最好改的 Java 组合技术栈选型直接决定你后面改代码的心情。常见的 Java 毕设组合有三种我按维护成本排个序。组合优点缺点SSM JSP教程资料最多老项目模板多JSP 前后端耦合改样式要重启答辩时显得过时Spring Boot Thymeleaf前后端不分离但页面是 HTML改完刷新即生效异步交互要写 AJAX稍微绕一点Spring Boot Vue最接近工业界做法要部署前端工程答辩要额外解释跨域和接口文档我的建议是第三种里的简化版Spring Boot 2.7.x MyBatis-Plus 3.5.x Thymeleaf Bootstrap。选 MyBatis-Plus 的核心原因是单表 CRUD 完全不用写 SQL内置分页插件生成器可以一次性生成 entity、mapper、service、controller 四层代码。做毕设最怕的不是业务难而是时间耗在“写 50 条重复的增删改查”上用 MyBatis-Plus 能把这部分时间从两周压缩到两天。版本上有个血泪经验不要用 Spring Boot 3.x它把 javax.servlet 换成了 jakarta.servlet很多网上教程的 import 语句直接报错代码和资料对不上时你会在排查环境上浪费大量时间。Spring Boot 2.7.x 是稳的JDK 用 1.8 或者 11 都行。如果用 Thymeleaf 做页面记得在 pom.xml 里加 spring-boot-starter-thymeleaf不要自己去引旧版的 thymeleaf-spring4依赖冲突会让人崩溃。2.3 数据库表设计一张总表看清核心表和它们的外键关系数据库是整个系统的地基表设计得当后面代码少写一半。我习惯先画一张表关系汇总表给每张表定好职责再写建表 SQL。表名用途关键字段外键关系sys_user系统登录用户username, password, role无material_category物资分类category_name无supplier供应商supplier_name, contact, phone无material物资档案name, category_id, supplier_id, stock, min_stock, max_stock分类、供应商inbound_order入库单主表order_no, supplier_id, in_date, operator供应商inbound_item入库单明细order_id, material_id, quantity, price入库单、物资outbound_order出库单主表order_no, out_date, department, operator无outbound_item出库单明细order_id, material_id, quantity出库单、物资主表和明细表的拆分是这个设计的重点。如果只建一张入库表每条明细都要重复写单号、供应商、日期数据冗余很严重论文里也不好解释“一对多”关系。拆成主表和明细表后写论文的数据模型图时能画出经典的“主表1—明细N”结构这是评审老师比较认可的设计。字段类型上有几个建议所有单据号用 varchar(32) 而不是 int因为单号要体现业务含义比如 IN20250101001 表示 2025 年 1 月 1 日的第 1 张入库单数量字段用 int金额字段用 decimal(10,2)所有表都加上 create_time、update_time、is_deleted 三个公共字段is_deleted 为后续做逻辑删除留后路这个细节在第 5 章会详细讲。2.4 项目包结构controller、service、mapper、entity 怎么摆放才不迷路包结构是代码仓库里的“房间布局”布局乱了后面加功能都不知道文件往哪放。我按 Spring Boot 的标准分层来组织包名以 com.yourname.material 为例src/main/java/com/yourname/material/ ├── controller/ -- 接收前端请求不做业务判断 ├── service/ │ └── impl/ -- 业务逻辑和事务边界都在这里 ├── mapper/ -- 继承 BaseMapper 的接口 ├── entity/ -- 与数据库表对应的实体类 ├── config/ -- MybatisPlusConfig、WebMvcConfig ├── common/ -- Result、PageResult、自定义异常 └── MaterialApplication.javacontroller 只做三件事接收参数、调 service、返回 Result。业务判断全部下沉到 service 实现类里因为 Transactional 事务注解只有放在 service 方法上才生效。答辩时老师最爱问的一句话是“事务边界设在哪一层”如果你的 controller 里写满了库存扣减逻辑这个问题就答不上来。mapper 层除了一些多表联查需要写 Select 注解外单表操作完全不用写代码MyBatis-Plus 的 BaseMapper 已经提供了 insert、deleteById、selectPage 等常用方法。这个结构对应论文里的系统设计章节时架构图就画三层Controller 表示层、Service 业务层、Mapper 数据访问层。实体类在 entity 包里对应数据库表结构。前后端用 Thymeleaf 渲染的页面放在 src/main/resources/templates 下静态资源放在 static 下和 Java 代码解耦答辩时展示目录结构也清晰。3. 数据库与初始化数据建表 SQL、存储过程和统计视图一次到位3.1 建库建表 SQL字段类型、约束和索引怎么写才不会被答辩老师挑刺数据库脚本是压缩包里最先要检查的文件。很多同学拿到的 SQL 一执行就报错不是因为代码问题而是建表语句的字符集、外键写法和当前 MySQL 版本不兼容。先建库再建表顺序别乱CREATE DATABASE IF NOT EXISTS material_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE material_db; CREATE TABLE material_category ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 分类ID, category_name VARCHAR(50) NOT NULL COMMENT 分类名称, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT 逻辑删除0未删 1已删, PRIMARY KEY (id), UNIQUE KEY uk_category_name (category_name) ) ENGINEInnoDB COMMENT物资分类表;这段 SQL 里有几个容易被忽略的参数utf8mb4 是必须的否则中文和 Emoji 都可能插入失败DEFAULT CURRENT_TIMESTAMP 让创建时间由数据库自动维护代码里不用手动 setON UPDATE CURRENT_TIMESTAMP 会在更新行时自动刷新更新时间。逻辑删除字段 is_deleted 在建表时就预留后面所有删除操作都改成 UPDATE而不是真实 DELETE这是物资管理系统里保护业务数据的常用做法。接着建物资表和入库单表。物资表是核心外键和索引都集中在它上面CREATE TABLE material ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 物资ID, name VARCHAR(100) NOT NULL COMMENT 物资名称, spec VARCHAR(100) DEFAULT NULL COMMENT 规格型号, category_id INT UNSIGNED NOT NULL COMMENT 分类ID, supplier_id INT UNSIGNED NOT NULL COMMENT 供应商ID, stock INT NOT NULL DEFAULT 0 COMMENT 当前库存, min_stock INT NOT NULL DEFAULT 10 COMMENT 最低库存预警值, max_stock INT NOT NULL DEFAULT 1000 COMMENT 最高库存预警值, unit VARCHAR(10) NOT NULL DEFAULT 件 COMMENT 计量单位, price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 参考单价, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, is_deleted TINYINT NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY idx_category_id (category_id), KEY idx_supplier_id (supplier_id), CONSTRAINT fk_material_category FOREIGN KEY (category_id) REFERENCES material_category (id), CONSTRAINT fk_material_supplier FOREIGN KEY (supplier_id) REFERENCES supplier (id) ) ENGINEInnoDB COMMENT物资表;这里的外键一定要建有的同学怕麻烦故意省略但答辩老师翻表结构时看到缺外键会直接质疑数据的参照完整性。索引方面外键字段自动需要索引另外给 name 字段加普通索引方便后面模糊搜索字段多了查询快。stock 字段允许为负数吗不允许但 MySQL 的 CHECK 约束在 5.7 版本不生效所以业务层的扣减逻辑必须做边界判断这个坑在第 5 章详细说。入库单主表和明细表我合并成一段展示注意外键级联策略要用 RESTRICT不要用 CASCADE。单据一旦录入明细就不允许被删除否则库存流水就断了CREATE TABLE inbound_order ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 入库单ID, order_no VARCHAR(32) NOT NULL COMMENT 入库单号, supplier_id INT UNSIGNED NOT NULL COMMENT 供应商ID, in_date DATE NOT NULL COMMENT 入库日期, operator VARCHAR(50) NOT NULL COMMENT 经办人, remark VARCHAR(255) DEFAULT NULL COMMENT 备注, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_supplier_id (supplier_id), CONSTRAINT fk_inbound_supplier FOREIGN KEY (supplier_id) REFERENCES supplier (id) ) ENGINEInnoDB COMMENT入库单主表; CREATE TABLE inbound_item ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 明细ID, order_id INT UNSIGNED NOT NULL COMMENT 入库单主表ID, material_id INT UNSIGNED NOT NULL COMMENT 物资ID, quantity INT NOT NULL COMMENT 入库数量, price DECIMAL(10,2) NOT NULL COMMENT 入库单价, PRIMARY KEY (id), KEY idx_order_id (order_id), KEY idx_material_id (material_id), CONSTRAINT fk_inbound_item_order FOREIGN KEY (order_id) REFERENCES inbound_order (id), CONSTRAINT fk_inbound_item_material FOREIGN KEY (material_id) REFERENCES material (id) ) ENGINEInnoDB COMMENT入库单明细表;order_no 为什么要建成唯一索引因为单号是业务维度上的唯一标识加唯一索引可以防止应用层重复提交时产生两条相同单号的入库记录。明细表里金额字段用 decimal(10,2)禁止用 float 或 double浮点数在累计求和时会出现精度误差做报表时数据对不上。3.2 用存储过程造演示数据三分钟生成半年出入库流水跑通界面后你会发现仓库里空荡荡的没数据可展示。手工一条条插入物资再插入单据一晚上就搭进去了。我当时用存储过程生成演示数据效果非常好DELIMITER $$ CREATE PROCEDURE init_material_demo_data() BEGIN DECLARE i INT DEFAULT 1; DECLARE stock_idx INT DEFAULT 0; WHILE i 30 DO INSERT INTO material (name, spec, category_id, supplier_id, stock, min_stock, max_stock, unit, price) VALUES ( CONCAT(演示物资, i), CONCAT(规格, i), (i MOD 5) 1, (i MOD 8) 1, 20 i * 3, 10, 500, 件, ROUND(10 i * 2.5, 2) ); SET i i 1; END WHILE; -- 随机生成 200 条入库明细 SET i 1; WHILE i 200 DO INSERT INTO inbound_order (order_no, supplier_id, in_date, operator, remark) VALUES ( CONCAT(IN2025, LPAD(i, 6, 0)), (i MOD 8) 1, DATE_SUB(CURDATE(), INTERVAL (i MOD 180) DAY), 演示操作员, 初始化演示数据 ); INSERT INTO inbound_item (order_id, material_id, quantity, price) VALUES (i, (i MOD 30) 1, 10 (i MOD 50), ROUND(10 (i MOD 20) * 1.5, 2)); SET i i 1; END WHILE; END$$ DELIMITER ; CALL init_material_demo_data();这个存储过程的重点在于物资表和入库单之间用 CONCAT 和 LPAD 生成有规律的编号方便论文截图展示入库日期用 DATE_SUB(CURDATE(), INTERVAL (i MOD 180) DAY) 均匀分布在过去半年内这样报表按月份分组出来是连续的折线图库存字段初始值设成 20 到 110 之间避免后面演示出库时因为库存不足而翻车。生成数据后还要顺手把库存字段和流水对平否则系统里会出现“库存显示 100但入库流水总和只有 50”的账实不符现象。最简单的做法是执行一条 UPDATE 语句按物资汇总入库明细数量来刷新库存。这个操作做完后再去页面上看数据报表和列表就都真实了论文里的截图也经得起推敲。3.3 视图与统计 SQL月度入库量和库存预警怎么查报表模块是论文里的亮点也是答辩时老师会细看的部分。统计 SQL 不用多复杂关键是按月分组和条件过滤的写法要标准。月度入库量统计SELECT DATE_FORMAT(in_date, %Y-%m) AS month, COUNT(DISTINCT o.id) AS order_count, SUM(i.quantity) AS total_quantity FROM inbound_order o JOIN inbound_item i ON i.order_id o.id WHERE o.in_date DATE_SUB(CURDATE(), INTERVAL 6 MONTH) GROUP BY DATE_FORMAT(in_date, %Y-%m) ORDER BY month;DATE_FORMAT(in_date, %Y-%m) 把日期格式化成“2025-01”这样的字符串GROUP BY 按它分组后每个月就是一行。COUNT(DISTINCT o.id) 统计入库单数SUM(i.quantity) 统计入库总量两者放在一起能看出“单少数量多”还是“单多数量少”的业务特征。WHERE 里的时间过滤条件限制最近半年避免数据量大了之后页面加载慢。这条 SQL 的结果直接给前端 ECharts 的柱状图用JSON 格式就是 [{month: 2025-01, total_quantity: 230}, ...]。库存预警列表的 SQL 更简单但条件判断要注意SELECT m.name, m.spec, m.stock, m.min_stock, CASE WHEN m.stock m.min_stock THEN 库存不足 WHEN m.stock m.max_stock THEN 库存积压 ELSE 正常 END AS stock_status FROM material m WHERE m.is_deleted 0 AND (m.stock m.min_stock OR m.stock m.max_stock) ORDER BY m.stock ASC;CASE WHEN 在这里生成一个状态列页面直接渲染状态文字不用在 Java 代码里做 if-else 判断。这个写法的好处是 SQL 返回的结果已经具备展示语义controller 到前端都不需要二次加工。整个统计模块建议做成一个独立的 ReportController提供 /report/monthlyInbound 和 /report/stockAlert 两个接口前端用 AJAX 拉 JSON 渲染图表。答辩时先展示表格再展示图表一气呵成。4. 核心流程的 Java 实现登录拦截、库存事务和分页查询全动手4.1 项目骨架与 MyBatis-Plus 配置数据源、分页插件和一个通用返回体拿到压缩包的源代码后第一步不是跑页面而是检查 application.yml 里的数据源配置和 MyBatis-Plus 的版本匹配。我把配置项按“能跑的最小集合”列出来server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/material_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 thymeleaf: cache: false mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: isDeleted logic-delete-value: 1 logic-not-delete-value: 0map-underscore-to-camel-case 的作用是把数据库里的 category_id 自动映射到实体类的 categoryId不配这个所有带下划线的字段查询出来都是 null。logic-delete-field 配置是逻辑删除的关键配好后 MyBatis-Plus 的 deleteById 会自动转成 UPDATE is_deleted1selectList 会自动追加 WHERE is_deleted0全局生效不用每个 mapper 方法手动加条件。log-impl 配 StdOutImpl 后控制台会打印完整 SQL这是排查分页和条件查询有没有生效最快的方式。分页插件需要手动注册到 MyBatis-Plus 的拦截器里光配 yml 不够要写一个配置类Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }PaginationInnerInterceptor 是分页功能的核心没有它Page 参数传进 mapper 方法里只会查询全表。DbType.MYSQL 指定数据库方言分页插件会自动生成 SELECT COUNT(*) 和 LIMIT 语句。这个配置类放在 com.yourname.material.config 包下Spring Boot 启动时会自动扫描到。配好之后所有继承 BaseMapper 的接口都自动具备分页能力论文里写“基于 MyBatis-Plus 分页插件实现列表分页”就有代码依据了。4.2 入库出库接口Transactional 事务边界和库存字段的并发处理入库和出库是物资管理系统里最容易出 bug 的业务。最常见的翻车写法是先 SELECT 查当前库存判断够不够再 UPDATE 扣减。两个用户同时操作时判断完库存都是 100都执行扣减最后库存变成 80但实际应该扣 40。查出来 100 这件事本身没错错在“检查”和“更新”之间不是原子的。正确做法是把检查下推到 UPDATE 语句里用数据库的行锁来保证原子性。我写入库和出库共用一套模式先看入库 service 的实现Service public class InboundServiceImpl implements InboundService { Resource private InboundOrderMapper inboundOrderMapper; Resource private InboundItemMapper inboundItemMapper; Resource private MaterialMapper materialMapper; Override Transactional(rollbackFor Exception.class) public void createInbound(InboundDTO dto) { // 1. 保存入库单主表 InboundOrder order new InboundOrder(); order.setOrderNo(generateOrderNo(IN)); order.setSupplierId(dto.getSupplierId()); order.setInDate(dto.getInDate()); order.setOperator(SecurityUtils.getCurrentUsername()); inboundOrderMapper.insert(order); // 2. 保存明细并同步库存 for (InboundItemDTO item : dto.getItems()) { InboundItem detail new InboundItem(); detail.setOrderId(order.getId()); detail.setMaterialId(item.getMaterialId()); detail.setQuantity(item.getQuantity()); detail.setPrice(item.getPrice()); inboundItemMapper.insert(detail); // 3. 原子更新库存防止并发覆盖 int rows materialMapper.increaseStock(item.getMaterialId(), item.getQuantity()); if (rows 0) { throw new BizException(物资不存在或已被删除无法入库); } } } }Transactional(rollbackFor Exception.class) 是关键它保证主表、明细表、库存表三处写入要么全部成功要么全部回滚。如果明细插入一半失败主表也会跟着回滚不会出现“单子有但库存没加”的数据不一致。increaseStock 是 MaterialMapper 里自定义的方法Update(UPDATE material SET stock stock #{quantity} WHERE id #{id} AND is_deleted 0) int increaseStock(Param(id) Integer id, Param(quantity) Integer quantity);出库方法只是把 SQL 换成stock stock - #{quantity}并加上AND stock #{quantity}条件让数据库拒绝负数库存Update(UPDATE material SET stock stock - #{quantity} WHERE id #{id} AND is_deleted 0 AND stock #{quantity}) int decreaseStock(Param(id) Integer id, Param(quantity) Integer quantity);decreaseStock 返回 0 时说明库存不够service 层直接抛出业务异常提示“库存不足”不用在 Java 代码里加锁或加 synchronized。这个方案在并发场景下是安全的因为 UPDATE 语句本身会锁行第二个事务会等第一个事务提交后才执行此时库存已经被扣减它的 stock quantity 判断基于的是最新值。答辩老师问“你怎么处理并发”你答“用数据库行锁保证原子性”然后贴上这条 SQL就站得住了。4.3 分页与模糊搜索LambdaQueryWrapper 和 Page 对象怎么配合物资列表是系统里访问量最大的页面搜索加筛选加排序都要支持。MyBatis-Plus 的 LambdaQueryWrapper 能避免字符串拼接 SQL 的注入风险同时代码可读性也好。我通常把查询条件封装成一个 DTOcontroller 接收前端的 pageNum、pageSize、name、categoryId 四个参数GetMapping(/material/list) public ResultPageResultMaterial listMaterial( RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String name, RequestParam(required false) Integer categoryId) { PageMaterial page new Page(pageNum, pageSize); LambdaQueryWrapperMaterial wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(name), Material::getName, name); wrapper.eq(categoryId ! null, Material::getCategoryId, categoryId); wrapper.orderByDesc(Material::getUpdateTime); materialMapper.selectPage(page, wrapper); PageResultMaterial result new PageResult(); result.setTotal(page.getTotal()); result.setRecords(page.getRecords()); return Result.ok(result); }LambdaQueryWrapper 的第一个参数是“是否拼接这个条件”。StringUtils.hasText(name) 为 true 时才追加 LIKE 条件categoryId ! null 时才追加等值条件前端不传这两个参数时查询就是全表分页。wrapper.eq 和 wrapper.like 都使用预编译参数不会出现 SQL 注入。orderByDesc(Material::getUpdateTime) 按更新时间倒序刚修改过的物资排在最前面这是用户操作习惯上的默认预期。selectPage 执行后page.getTotal() 是符合条件的总记录数page.getRecords() 是当前页数据。前端 Thymeleaf 页面里用 JavaScript 渲染表格或者直接用 Thymeleaf 的 th:each 遍历 records。如果页面需要显示分页条PageResult 里可以再带上 pageNum、pageSize、totalPage 三个字段前端 Bootstrap 的 pagination 组件直接套用即可。这一通操作下来分页查询这块就完全覆盖论文里“系统采用分页技术缓解数据库压力”的功能描述。4.4 报表接口按月份分组的 SQL 与前端 ECharts 的数据格式对齐报表接口的本质是把统计 SQL 的结果转换成前端图表库能直接消费的 JSON。后端接口返回的格式我统一为[{month: 2025-01, total: 230}, ...]前端 ECharts 的 bar 图直接把这个数组填进 xAxis 和 series 就行。后端实现的核心是查询语句和结果封装GetMapping(/report/monthlyInbound) public ResultListMapString, Object monthlyInbound() { ListMapString, Object data inboundOrderMapper.selectMonthlyInbound(); return Result.ok(data); }mapper 里的 SQL 用 Select 注解直接写在接口方法上Select(SELECT DATE_FORMAT(in_date, %Y-%m) AS month, SUM(i.quantity) AS total FROM inbound_order o JOIN inbound_item i ON i.order_id o.id WHERE o.in_date DATE_SUB(CURDATE(), INTERVAL 6 MONTH) GROUP BY DATE_FORMAT(in_date, %Y-%m) ORDER BY month) ListMapString, Object selectMonthlyInbound();SQL 的列别名 month 和 total 对应 Map 的 keyMyBatis 会把查询结果转成 MapString, Object无需额外定义 VO 类。前端页面里用 axios 或 jQuery 的 $.ajax 请求这个接口拿到数据后设置 ECharts 的 xAxis.data 和 series.data。需要注意的一点是ECharts 的柱状图 x 轴默认要求字符串数组如果数据库返回的 month 是 Date 类型会让图表 x 轴显示成时间戳所以 SQL 里一定用 DATE_FORMAT 先转成字符串。报表模块做完后在论文里截图时选一个有明显涨落的月份区间视觉上更有说服力。5. 物资管理系统避坑手册从数据库死锁到答辩追问的 5 个真实翻车点5.1 坑一库存扣减出现负数并发出入库把库存改没了现象两个人同时对一个物资做出库操作明明库存只剩 10 件两张单据各出 8 件最终库存却变成了 -6页面列表出现负数库存。原因代码里用的是“先查询库存判断足够后再 UPDATE”的两步操作。第一步两个事务都读到库存 10都认为可以扣减第二步各自把库存改成 2后提交的那个事务覆盖了先提交的结果实际只扣了 8 件而不是 16 件更严重的场景是两个事务都按“库存10-8”计算最后库存 2看起来像只扣了 8 件。这是典型的 check-then-act 竞态条件。解决把判断下推到 UPDATE 语句里用UPDATE material SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity}通过受影响行数判断是否扣减成功。row 0 就抛异常提示库存不足整个事务回滚。这个写法是数据库自己加的行锁并发安全且没有死锁性能也好。千万别在 service 里用 synchronized 锁方法那样只能锁单实例多实例部署时依然会出错。5.2 坑二删除物资时外键约束报错页面直接回滚 500现象在物资管理页面点击删除分类或供应商页面直接跳 500 错误控制台提示 Cannot delete or update a parent row: a foreign key constraint fails。原因material 表通过 category_id 和 supplier_id 建立了外键删除分类时分类表里还有物资引用它数据库出于参照完整性保护拒绝了 DELETE。解决不要物理删除启用 MyBatis-Plus 的逻辑删除。实体类字段加 TableLogic 注解yml 里配好 logic-delete-field删除操作自动变成 UPDATE is_deleted 1。这样外键永远指向一个“逻辑存在”的记录不会触发外键约束。如果必须物理删除比如清库那要先删除引用它的明细表和子表数据再用事务包裹顺序不能反。毕设阶段强烈建议用逻辑删除论文里还能写一句“系统支持数据恢复功能”。5.3 坑三SQL 脚本导入 MySQL 5.7 就失败字符集和表名都有讲究现象压缩包里的 SQL 脚本在 MySQL 5.7 上执行报错语法检查不过在 Windows 上跑得好好的系统部署到 Linux 后提示找不到表 material。原因建表语句里用了 MySQL 8.0 才支持的语法或者写了不生效的 CHECK 约束5.7 解析失败另一个原因是 Windows 的文件系统不区分大小写而 Linux 严格区分代码里写的类名 Material 和表名 material 对应不上时Linux 上就查不到表。解决建表脚本统一使用 utf8mb4 字符集去掉所有 CHECK 约束时间默认值统一用 DEFAULT CURRENT_TIMESTAMP。所有表名、字段名一律小写下划线Java 属性用驼峰靠 map-underscore-to-camel-case 自动映射。连接 MySQL 8 的 driver-class-name 是 com.mysql.cj.jdbc.Driver连接 5.7 则用 com.mysql.jdbc.Driver如果数据库版本和驱动不匹配启动时会报 Failed to configure a DataSource 或者 java.sql.SQLException。5.4 坑四论文里的架构图和实际代码分层对不上现象答辩 PPT 里画的是经典三层架构但代码里 controller 写了 200 行业务判断service 层空空的老师问“事务在哪一层”回答不上来。原因先画架构图后写代码写的过程中为了省事把逻辑直接堆在 controller最后没有回头改图图变成了一张空头支票。解决以实际代码为准重画架构图。架构图上的箭头方向要正确Controller 调 ServiceService 调 MapperMapper 操作数据库。如果代码已经在 controller 里写了业务答辩前花半天时间把逻辑下沉到 servicecontroller 只保留参数接收和 Result 返回。这个重构本身并不复杂因为 MyBatis-Plus 的 mapper 已经是独立的迁移业务逻辑只是搬方法。PPT 上技术栈一栏写 Spring Boot MyBatis-Plus Thymeleaf MySQL不要写“SSM 框架”这种和代码明显矛盾的词老师扫一眼就能看出区别。5.5 坑五用户密码明文存储评审老师一眼看出安全问题现象数据库的 sys_user 表里密码字段存的是 123456、admin 这样的明文老师看数据库截图时直接指出“你这密码没加密”。原因为了省事注册和登录的密码都是直接 insert 明文没做任何处理。答辩时安全这块是高频发问点一旦被问住后面回答都会减分。解决密码加密用 BCrypt。项目里单独引入 spring-security-crypto 依赖不需要引入整个 Spring Security避免过滤器拦截所有请求的麻烦。注册时String hash new BCryptPasswordEncoder().encode(rawPassword)登录时用encoder.matches(rawPassword, dbPassword)校验。BCrypt 会自动生成随机盐相同明文每次加密结果都不同安全性有保障代码量也小。论文的安全设计章节里写上“系统采用 BCrypt 加密算法对用户口令进行不可逆加密存储”这句话就是加分项。6. 交付前最后一小时用一条 SQL 验证一致性再把答辩演示走一遍所有功能都跑通之后不要急着打包压缩先做一次数据一致性体检。库存数量和出入库流水对不上的问题在答辩演示时一旦出现之前所有讲解都会失去可信度。我最常用的验证 SQL 是SELECT m.id, m.name, m.stock AS current_stock, (SELECT IFNULL(SUM(i.quantity), 0) FROM inbound_item i WHERE i.material_id m.id) - (SELECT IFNULL(SUM(o.quantity), 0) FROM outbound_item o WHERE o.material_id m.id) AS flow_stock FROM material m WHERE m.stock flow_stock;这条 SQL 把每件物资的系统库存字段和出入库流水汇总做差返回空结果就说明账实相符。若返回有数据就在答辩前用恢复库存的 UPDATE 修平。运行完这条 SQL 后我个人习惯再跑一遍月度报表接口确认六个月的数据是连续的因为报表断了一个月老师很容易看出图表中间缺了一块。答辩演示的操作顺序也是练出来的不是临时翻代码。我的习惯是先讲 1 分钟背景和模块图再花 5 分钟演示登录、新增物资、做一张入库单、做一张出库单然后到库存查询页展示数量和预警状态最后展示月度报表的柱状图。每个环节停 3 秒让页面加载完不要急着点下一步。数据库中准备一个“库存不足”的物资演示预警时直接出现红色提示这个效果比任何口头解释都有说服力。密码加密那些安全措施建议在答辩的高阶问题环节主动提一句作为技术亮点的补充。如果有人问到并发就把第 4 章的 decreaseStock SQL 写出来讲问到外键就讲逻辑删除。把这些问题想一遍你会发现整套系统经得起追问。我当年第一次做毕设就是栽在 PPT 架构图和实际代码不一致上被老师追问了整整五分钟场面一度很尴尬。现在每次交付前我都会把架构图对着代码过一遍把这条一致性 SQL 跑一遍把演示流程完整走一遍这三件事做完才能安心提交压缩包。这次物资管理系统从论文、答辩PPT、源代码到数据库脚本的整个交付链路希望这些方法能帮你提前避开同样的坑希望帮到你。本文还有配套的精品资源点击获取
返回列表