
1. 项目定位与核心需求拆解煤矿物资智能管理系统听着名字挺长拆开看其实就是一套典型的“进销存 审批流 预警”业务系统只是把场景换到了煤矿行业。煤矿物资管理跟普通仓库管理的区别在哪普通仓库管的是SKU、批次、效期煤矿物资管的是安全、急缺、账实相符。一个螺丝钉缺了可能没关系但井下通风用的关键配件缺了是要停产甚至出安全事故的。所以这类系统的核心需求我归类成四个维度第一物资台账数字化。所有物资从入库、出库、领用、归还、报废全过程要有电子记录替代原来的手工账本和纸质单据。这一条看起来简单但实际上是整个系统的地基。很多毕设项目一上来就写功能结果库存台账字段设计得乱七八糟后面做报表、做预警全都不好使。第二库存实时可视化。管理者和库管员需要随时知道仓库里有什么、有多少、在哪、是否临期、是否超储。这个需求对应的技术点就是库存查询、物料分类统计、低库存预警、效期预警。第三审批流程规范化。区队申请领料、材料员审核、库管员发货这个流程在煤矿上是有严格的权限要求的。谁申请、谁审批、谁能超量领用这些权限如果只靠线下盖章签字效率低还容易出错。系统里要设计多级审批流至少做到申请单状态可追踪、审批记录可回溯。第四数据支撑决策。月底对账、季度盘点、年度采购计划这些都需要历史数据来做支撑。系统要有报表统计能力哪怕只是简单的按物资类别汇总、按领用部门汇总也能给物资管理人员提供直接的决策依据。从毕设角度来说这类系统的典型功能模块可以划成系统管理用户、角色、菜单、基础资料供应商、物资分类、物料档案、采购管理采购计划、采购入库、库存管理入库、出库、盘点、调拨、领用管理申请、审批、发料、统计报表、预警提醒。就这么七块功能量适中既能体现工作量又不至于做不完是毕设选题里非常稳妥的类型。2. 技术选型与工程结构设计技术栈选择我建议直接走后端分离开源方案Spring Boot MyBatis-Plus MySQL Vue 3 Element Plus。为什么这么选首要原因是生态成熟、资料好找、排错容易。Spring Boot是目前Java领域做毕设和中小型项目的最主流框架没有之一。MyBatis-Plus把单表CRUD的代码量压缩到极致一张表一个实体类一个Mapper接口就能完成基础增删改查省下来的时间全都可以投入到业务逻辑和页面打磨上。前端Vue 3搭配Element Plus组件丰富写后台管理系统跟搭积木一样三天就能把界面框架搭起来。数据库选MySQL而不是PostgreSQL或者SQLite也是考虑到资料全面性和后续扩展。MySQL在Windows和Linux上都能一键安装Navicat可视化操作方便网上解决方案一搜一大把。对于毕设阶段来说稳定性和易用性比性能上的细微差距重要得多。工程结构方面后端包结构我推荐这样的分层com.coalmine.material ├── controller // 接口层接收请求、返回结果 ├── service // 业务层处理核心逻辑 │ └── impl // 业务实现类 ├── mapper // 数据访问层MyBatis-Plus接口 ├── entity // 数据库实体类 ├── dto // 传输对象接收前端参数 ├── vo // 视图对象封装返回给前端的数据 ├── config // 配置类如MyBatisPlus分页配置、跨域配置 ├── common // 通用类如统一返回结果、异常处理 └── utils // 工具类如日期处理、唯一编号生成这种分层的好处熟练的开发会觉得“这不废话吗标准三层架构”但对于没怎么做过完整项目的同学来说最大的意义是每一层的职责边界清晰出bug的时候知道去哪里找写代码的时候知道该往哪个文件里写。我见过太多同学把业务逻辑全写在Controller里一个方法几百行后面查问题的时候痛苦到怀疑人生。前端工程结构用Vite脚手架初始化目录大致是src ├── api // 接口请求封装 ├── router // 路由配置 ├── views // 页面组件 ├── components // 公共组件 ├── stores // Pinia全局状态 └── utils // 工具类如axios封装前后端分离部署前端开发时用Vite代理转发接口到后端8080端口避免跨域问题// vite.config.js server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这套方案搭下来项目的启动速度、迭代速度、可维护性都在线而且面试答辩的时候也有东西可讲——分层架构、前后端分离、接口设计这些都是加分项。3. 数据库设计——系统的地基数据库设计是这类管理系统成败的关键没有之一。很多出问题的毕设项目八成都是表结构设计不合理。要么字段缺东少西要么没有考虑业务状态流转要么主外键关系混乱。我的建议是数据库设计宁可多花一天也不要做到一半推倒重来。3.1 核心表设计整个系统我规划了这些核心表用户相关sys_user用户表id, username, password, nickname, dept_id, role_id, status, create_time sys_role角色表id, role_name, role_code, description sys_menu菜单权限表id, menu_name, parent_id, path, component, perms sys_user_role用户角色关联表user_id, role_id密码存的是BCrypt加密后的密文不是明文。这点必须注意数据库一旦泄露明文密码就是安全事故。Spring Security自带BCryptPasswordEncoder直接拿来用就行。物资相关base_supplier供应商表id, supplier_name, contact_person, contact_phone, address, status base_material_category物资分类表id, category_name, parent_id, sort_order base_material物料档案表id, material_code, material_name, category_id, spec_model, unit, safe_stock, max_stock, default_price, status物料档案表是整个库存管理的核心主数据。material_code要设计编码规则比如“产物资-备件-井下-001”用拼音首字母或者数字分段来标识这样查询和盘点时能快速定位。业务表stock_warehouse仓库表id, warehouse_name, location, manager stock_inbound入库单表id, inbound_no, supplier_id, warehouse_id, inbound_type, total_amount, status, create_by, create_time stock_inbound_item入库单明细表id, inbound_id, material_id, quantity, unit_price, amount, batch_no, production_date, expire_date stock_outbound出库单表id, outbound_no, warehouse_id, outbound_type, applicant_id, dept_id, status, approve_by, approve_time, create_time stock_outbound_item出库单明细表id, outbound_id, material_id, quantity, unitprice, remark stock_current实时库存表id, warehouse_id, material_id, quantity, locked_quantity, update_time apply_record领用申请单表id, apply_no, dept_id, applicant_id, status, approve_by, approve_time, apply_time apply_record_item领用申请明细表id, apply_id, material_id, apply_quantity, approved_quantity这里有一条非常重要的设计原则实时库存stock_current和流水记录入库单、出库单明细必须分开。库存表只负责存“当前还有多少”流水表负责存“每一次进出变化”。程序里的库存变动要先写流水再更新库存表并且要放在同一个数据库事务里。这样才能保证账实相符、可追溯。3.2 编码规则设计单据编码要设计得有意义。我不建议直接用自增ID当单号因为在实际使用中业务员不可能记“第12345号的入库单”但说“今天8月3日的第三张入库单”他是有概念的。所以单号规则建议这样入库单号RK 年月日 三位流水如 RK20250612001 出库单号CK 年月日 三位流水 领用申请单号LY 年月日 三位流水 物料编码类别码 流水号如 WZFJ001代表“物资-非金属类”Java里生成单号的核心代码很简单public String generateInboundNo() { String dateStr LocalDate.now().format(DateTimeFormatter.ofPattern(yyyyMMdd)); String prefix RK dateStr; QueryWrapperStockInbound wrapper new QueryWrapper(); wrapper.likeRight(inbound_no, prefix).orderByDesc(inbound_no).last(limit 1); StockInbound last stockInboundMapper.selectOne(wrapper); int seq 1; if (last ! null) { String lastNo last.getInboundNo(); seq Integer.parseInt(lastNo.substring(lastNo.length() - 3)) 1; } return prefix String.format(%03d, seq); }这里有个小坑要注意并发情况下可能出现重复单号。毕设阶段并发量可以忽略但如果想做得更严谨可以对单号生成方法加synchronized锁或者在数据库层给inbound_no字段加唯一索引兜底。3.3 关键索引设计索引不是越多越好但核心查询字段一定要建索引。我建议至少给这些字段建索引所有单据表的单号字段唯一索引物料表的material_code唯一索引实时库存表的warehouse_id material_id联合索引单据明细表的material_id普通索引创建时间字段create_time普通索引建索引的意义在数据量小的时候感受不到但到了5万条以上就会非常明显。我见过同学用Navicat打开带10万条数据的表执行全表扫描卡到怀疑电脑配置其实就是少了索引。4. 核心功能模块实现要点4.1 登录认证与权限控制登录模块是所有模块的基础。我用的是JWT Spring Security方案。用户输入账号密码后端校验通过后生成一个token返回前端前端把token存到localStorage每次请求在header里带上后端通过过滤器解析token识别用户身份。流程是这样的用户提交username和password后端从数据库查出用户信息用BCrypt校验密码校验通过后生成JWTpayload里装userId、username、role等信息前端拿到token后存储同时根据用户角色动态渲染菜单后端在接口上加PreAuthorize注解控制权限后端核心过滤器逻辑Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); try { Claims claims JwtUtil.parseToken(token); String username claims.getSubject(); if (username ! null SecurityContextHolder.getContext().getAuthentication() null) { UserDetails userDetails userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken auth new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(auth); } } catch (Exception e) { // token无效或过期继续走匿名访问 } } chain.doFilter(request, response); } }这里我踩过一个坑token有效期设太短用户操作一会儿就要重新登录设太长安全问题又来了。平衡方案是access_token有效期2小时前端在请求返回401时自动刷新token。刷新token用独立的refresh_token有效期7天存到数据库里可以主动失效。4.2 入库管理——核心链路入库流程是整个物资流转链条的入口直接影响后续所有环节的数据准确性。入库方式通常有两种采购入库和退库入库。采购入库的场景是采购员根据采购计划到货库管员录入采购单核对物料、数量、单价生成入库单自动增加库存。退库入库的场景是井下班组把未使用完的物资退回仓库重置库存。入库单的保存逻辑我分为两步第一步保存入库单主表。状态为“待审核”。第二步保存入库明细表。遍历前端传来的明细列表逐条写入stock_inbound_item表。第三步审核通过后更新实时库存。代码逻辑如下Transactional(rollbackFor Exception.class) public void approveInbound(Long inboundId) { StockInbound inbound stockInboundMapper.selectById(inboundId); if (inbound null || !0.equals(inbound.getStatus())) { throw new BizException(入库单不存在或已审核); } ListStockInboundItem items stockInboundItemMapper.selectList( new QueryWrapperStockInboundItem().eq(inbound_id, inboundId)); for (StockInboundItem item : items) { // 检查当前仓库是否有该物料库存记录 StockCurrent current stockCurrentMapper.selectOne(new QueryWrapperStockCurrent() .eq(warehouse_id, inbound.getWarehouseId()) .eq(material_id, item.getMaterialId())); if (current null) { current new StockCurrent(); current.setWarehouseId(inbound.getWarehouseId()); current.setMaterialId(item.getMaterialId()); current.setQuantity(item.getQuantity()); current.setLockedQuantity(0); stockCurrentMapper.insert(current); } else { current.setQuantity(current.getQuantity() item.getQuantity()); stockCurrentMapper.updateById(current); } } inbound.setStatus(1); inbound.setApproveTime(LocalDateTime.now()); stockInboundMapper.updateById(inbound); }两个关键点第一Transactional注解必须加。整个审核过程涉及多张表更新任何一个环节出错都必须回滚不然就会出现入库单状态是“已审核”但库存没加上的严重数据不一致问题。第二先查库存再更新的顺序要注意并发问题。如果同一时间两个入库单都审核同一个物料最后更新的是最后一次查询的结果前面的更新会丢失。更严谨的做法是用UPDATE stock_current SET quantity quantity #{quantity} WHERE id #{id}这种原子SQL代替先查后改。4.3 领用申请与审批流领用流程是煤矿物资管理里最有业务特色的模块对应的技术点是审批状态流转。状态设计0-待审核 → 1-审核通过 → 2-已发料 → 3-已驳回 → 4-已取消审批逻辑要点同一角色不同等级的人审批权限不同。这里我没用复杂的Activiti或者Flowable工作流引擎毕设阶段引入工作流引擎反而是负担。我用的是“角色-状态机”策略申请单提交给所在区队的队部审批队部审核通过后如果是普通物资直接流转到仓库发料如果是定额物资或者金额超过阈值需要再经过材料科审批。判断逻辑在Service层单独写清晰易懂public void approve(Long applyId, Long approverId, boolean pass, String comment) { ApplyRecord apply applyRecordMapper.selectById(applyId); // 校验当前用户是否有权限审批该单 if (!hasApprovePermission(apply, approverId)) { throw new BizException(当前用户无审批权限); } if (pass) { if (0.equals(apply.getStatus())) { // 第一级审批通过后判断是否需要二级审批 if (needSecondApprove(apply)) { apply.setStatus(1_1); // 待二级审批 } else { apply.setStatus(1); } } else if (1_1.equals(apply.getStatus())) { apply.setStatus(1); } } else { apply.setStatus(3); } apply.setApproveBy(approverId); apply.setApproveTime(LocalDateTime.now()); apply.setApproveComment(comment); applyRecordMapper.updateById(apply); }这个设计的巧妙之处在于后续如果加一个新的审批层级只需要在状态机里加一个状态值、在needSecondApprove方法里加一行判断不用改动其他代码。领用申请审核通过之后仓库执行发料发料时同样走事务更新库存扣减、关联出库单、修改申请单状态为“已发料”。这里要注意校验库存量不足时不允许发料前端要提示申请不相符后端也必须兜底校验。4.4 库存查询与预警库存查询分两种常见场景按物料查和按仓库查。按物料查输入物料名称/编码的关键字返回所有仓库的库存分布。按仓库查选中某个仓库看该仓库下全部物料的库存情况。这两种场景用MyBatis-Plus的QueryWrapper都能实现但真正难的是组合查询。我建议封装一个查询条件DTOpublic class StockQueryDTO { private String warehouseId; private String categoryId; private String keyword; // 物料名称/编码模糊查询 private Integer lowStockFlag; // 只看低库存1只看低库存 0全部 private Integer pageNum; private Integer pageSize; }Mapper层写SQL关联查询select idselectStockList resultTypecom.coalmine.material.vo.StockVO SELECT sc.id, sc.warehouse_id, w.warehouse_name, sc.material_id, m.material_code, m.material_name, m.spec_model, m.unit, m.safe_stock, m.max_stock, sc.quantity FROM stock_current sc LEFT JOIN base_material m ON sc.material_id m.id LEFT JOIN stock_warehouse w ON sc.warehouse_id w.id WHERE 11 if testwarehouseId ! null and warehouseId ! AND sc.warehouse_id #{warehouseId} /if if testcategoryId ! null and categoryId ! AND m.category_id #{categoryId} /if if testkeyword ! null and keyword ! AND (m.material_name LIKE CONCAT(%, #{keyword}, %) OR m.material_code LIKE CONCAT(%, #{keyword}, %)) /if if testlowStockFlag ! null and lowStockFlag 1 AND sc.quantity lt; m.safe_stock /if /select预警的逻辑也简单每次更新库存之后对比实时库存与安全库存。如果当前库存低于安全库存值系统生成一条预警记录管理员登录后在首页看到预警卡片。为了防止重复预警预警记录表要加一个“是否已处理”字段状态为“已处理”的预警不再重复生成。4.5 物资分类树与编码唯一性物资分类这块有一个容易忽略的细节分类层级不能太深建议最多两级。一级分类如“综采配件、综掘配件、通风材料、运输材料、支护材料、电气材料、劳保用品、其他”二级分类在下面维护子类。分类太深会导致业务人员录入物料时选择困难反而降低效率。物料编码唯一性校验要在两个层面同时控制第一层数据库层面。base_material表的material_code字段加unique约束。第二层代码层面。保存物料信息之前先查一下Long count baseMaterialMapper.selectCount(new QueryWrapperBaseMaterial() .eq(material_code, material.getMaterialCode())); if (count 0) { throw new BizException(物料编码已存在请检查后重新填写); }两个层面都要加数据库约束是兜底代码提示是给用户友好体验。5. 前端页面设计与交互优化5.1 页面结构规划前端页面按照角色视角来组织而不是功能堆砌。登录后的首页是数据看板展示库存总量、低库存预警数、待审批任务数、近30天入库出库趋势。这些数据统计接口后端一次性返回前端ECharts直接渲染效果非常直观。菜单规划如下领导/管理者数据看板、库存总览、审批中心、报表统计库管员入库管理、出库管理、库存管理、盘点管理、预警处理、供应商管理、物料管理区队材料员领用申请、我的申请菜单权限控制用Vue Router的路由守卫来实现根据用户角色渲染不同的菜单栏和后端返回的路由表。5.2 关键页面细节入库单页面主表信息是一个表单供应商、仓库、入库类型明细部分是一个可编辑表格每行选择物料后自动带出物料编号、规格、单位填写数量单价后自动计算金额。这里有一个提高体验的小技巧物料选择用远程搜索的下拉框输入关键字从后端模糊搜索物料而不是把全部物料一次性加载到前端。数据量小的时候无所谓物料到几千条的时候一次加载页面直接卡死。领用申请页面申请单选择部门后自动带出该部门的常用物资清单减少重复输入。这里可以做“模板”功能每个区队维护一个“常用物资模板”新建申请单时一键填充模板中的物资列表只需修改数量即可提交。库存列表页面表格按物料分类分组展示每个分类折叠收起方便库管员快速定位。列表行内直接显示低库存预警红色标记和超储物料的橙色标记。5.3 数据交互与体验优化前后端接口交互我用的是axios封装统一处理// request.js import axios from axios import { ElMessage } from element-plus import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res.data }, error { if (error.response error.response.status 401) { ElMessage.error(登录已过期请重新登录) localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } )这段代码解决的是前端最容易出现的三个问题统一的接口地址前缀、一致的错误提示、登录过期的自动跳转。写完之后业务页面里只需要一行const data await request.get(/stock/list, { params })就能完成请求和响应处理。6. 系统安全与细节处理毕设项目虽然不用做到企业级安全标准但有些底线还是要守住的。6.1 参数校验不能省前端传过来的数据后端必须做校验。我见过太多项目前端把必填校验写了后端就完全不校验结果有人绕过前端直接调接口数据就全乱了。后端建议用Spring Validation注解做Bean Validationpublic class InboundDTO { NotNull(message 供应商不能为空) private Long supplierId; NotNull(message 仓库不能为空) private Long warehouseId; NotEmpty(message 入库明细不能为空) private ListInboundItemDTO items; }6.2 SQL注入防范MyBatis-Plus的QueryWrapper和MyBatis的#{}占位符天然防SQL注入但前提是不要用${}拼字符串。尤其是模糊搜索场景用CONCAT拼接可以安全处理AND m.material_name LIKE CONCAT(%, #{keyword}, %)不要写成AND m.material_name LIKE %${keyword}% !-- 错误示范存在SQL注入风险 --6.3 金额精度问题钱相关字段统一用DECIMAL类型Java里用BigDecimal不要用double或者float。这个经验是很多新手会踩的坑double做浮点运算会出现0.10.2不等于0.3的问题做金额累计差一分钱对账半天。6.4 操作日志物资系统的数据非常敏感年底审计的时候要能查“谁在什么时候改了哪张单”。我的做法是给关键操作加日志记录Aspect Component public class OperationLogAspect { Around(annotation(operationLog)) public Object recordLog(ProceedingJoinPoint joinPoint, OperationLog operationLog) throws Throwable { String module operationLog.module(); String action operationLog.action(); // 记录操作人、操作时间、操作内容、IP地址 // 使用系统时间LocalDateTime.now() Object result joinPoint.proceed(); return result; } }入库、出库、审核、驳回、修改物料、删除物料这些操作都必须有日志。日志表字段id、operator_id、operator_name、module、action、content、ip、create_time。7. 常见问题及排查经验7.1 日期类型转换问题前端传日期字符串“2025-06-12”到后端LocalDate和LocalDateTime在Jackson反序列化时会报错或者解析成null。解决办法是在实体类的日期字段上加上统一的格式化注解JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createTime; JsonFormat(pattern yyyy-MM-dd, timezone GMT8) private LocalDate productionDate;同时建议在application.yml里做全局配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT87.2 低库存预警重复生成预警逻辑放在查询接口里不是好的做法因为只有用户登录、访问页面才会触发检查。更合理的做法有两个方向一是入库/出库操作完成后触发检查。每次库存变更后判断是否低于安全库存生成预警记录。二是利用定时任务。Spring Boot里用Scheduled(cron 0 0 8 * * ?)每天早晨8点执行一次库存检查将低于安全库存的物资生成预警记录。我实际项目中用的是方式一加方式二的组合变更时即时预警每天定时补漏。这样既保证了实时性又兜底了因为历史数据错误或安全库存调整导致的漏检。7.3 导出Excel乱码报表导出Excel用了EasyExcel导出中文文件名时浏览器会出现乱码。解决办法是设置响应头时做URL编码String fileName URLEncoder.encode(库存报表, UTF-8).replaceAll(\\, %20); response.setHeader(Content-Disposition, attachment; filename*UTF-8 fileName);7.4 部署到服务器后接口404本地开发一切正常部署到服务器后接口404。排查思路先看dist目录是否正确打包再看nginx或Tomcat的静态资源路径配置。如果是打成Jar包运行注意把前端dist目录放到static或public目录下。8. 测试用例设计与验收准备8.1 核心业务场景测试给自己列一个测试清单每个场景都要验证通过后才能算完成入库流程新增入库单→审核→库存增加→查看明细 出库流程新增出库单→校验库存充足→审核→库存扣减→查看明细 领用流程区队提交申请→队部审核→仓库发料→库存扣减 库存预警将安全库存设为100库存降至80→预警记录生成→处理预警 权限控制库管员账号登录访问管理页面→提示无权限9. 我的实操经验与结语最后聊点过来人的经验。做这类管理系统最大的坑不是技术问题而是需求理解偏差。很多同学一上来就写代码写到一半发现功能不对推倒重来。我建议动手前一定先梳理清楚业务流程哪怕画个流程图也行把“谁申请、谁审批、谁入库、谁出库”走一遍远比直接打开IDEA写CRUD有意义。第二点经验是善用开源轮子但一定要看懂原理。类似的项目在GitHub上能找到很多参考源码可以参考代码风格和表结构设计但核心的模块代码一定要自己写一遍。这不只是为了应付查重更重要的是答辩时老师问“这个入库审核为什么要用事务”“这个权限是怎么控制的”只有自己真正写过、踩过坑才能答得出来。第三点命名规范这件事值得单独说一说。很多同学的代码里充斥着a、b、c、list1、list2这类变量名等过两周自己回来看都懵了。哪怕是小项目变量名、方法名、类名都要用有意义的英文命名单词别拼错。这会直接影响代码的质量评估和可维护性有时候导师看一眼代码就能判断你有没有认真做。这套煤矿物资智能管理系统技术上用的是常规的Spring Boot Vue组合没有炫技但把进销存、审批流、权限控制、库存预警、报表统计这些核心逻辑做扎实就是一个完成度很高的毕设项目也具备真实的使用价值。我个人是建议未来可以把它扩展成多仓库模式增加供应商门户或者移动端审批毕业之后继续维护做成一个真正能落地的小产品价值会更大。