Java生产管理系统源码拆解:Spring Boot工单库存与权限实战 简介这是一套面向计算机相关专业毕业生与Java入门开发者的生产管理系统毕业设计程序围绕生产管理业务场景提供可运行的完整实现适合用于课程设计、毕业设计参考与项目实战练习。压缩包共274个文件约1.14MB包含102个Java源文件、68个Vue页面与组件、26个class文件、22个XML配置及SQL建库脚本、属性配置、前端静态资源等后端与前端代码、数据库文件配套齐全并附环境配置说明。已有519人学习下载。程序覆盖需求分析、总体设计、数据库访问、功能模块实现与测试总结等环节可见用户管理、权限拦截、分页查询、文件上传等典型模块读者可据此理解Java Web项目的分层结构、接口组织与数据库设计方法也便于对照修改功能、完善论文与准备答辩。1. 从一份跑不起来的毕设源码说起java生产管理系统到底管了哪些事去年帮一个朋友看他从网上扒下来的 java生产管理系统 毕业设计程序压缩包解压出来七十多个 Java 文件application.yml 里数据库密码还是作者的本地配置前端页面能打开但一登录就 401。他问我这玩意儿到底能不能用我花了一个下午把表结构和 Controller 翻了一遍结论是能用而且业务闭环相当完整。这套系统围绕一条主线展开生产计划排下去、生成工单、按 BOM 扣物料、入库出库记流水、最后出产量报表外围再挂一套用户角色菜单权限。它适合两类人一类是拿它当毕设底稿、想快速改出自己东西的学生另一类是工作一两年、想找一个业务分层写得规矩的 Spring Boot 项目照着练手的开发者。下面按我自己拆项目的顺序从技术栈、目录、核心链路一直讲到怎么跑、怎么改。2. 技术栈与工程结构Spring Boot MyBatis MySQL 这套组合怎么分工拿到一套陌生源码我一般不看业务代码先看 pom.xml 和目录。依赖能告诉你作者的技术年代目录能告诉你他的分层习惯这两样看完后面读代码基本就是填空。2.1 技术选型四层依赖各自负责什么这套系统的技术组合是比较典型的“能跑就行”派后端 Spring Boot 2.x 打底持久层 MyBatis 加 XML 写 SQL数据库 MySQL前端要么是 Thymeleaf 加 Layui要么是独立拆出去的 Vue 加 Element UI。两种形态我都见过区别只在前端是不是单独一个工程。层次常见选型在这套系统里负责什么后端框架Spring Boot 2.x启动、自动装配、Web 层、事务管理持久层MyBatis Mapper XML工单、库存、报表这些复杂 SQL 手写数据库MySQL 5.7 / 8.0业务表 权限表 流水表前端Layui 或 Vue Element UI列表、表单、ECharts 报表鉴权JWT 或 Shiro登录态、接口拦截、菜单权限工具Lombok、Hutool简化实体类、日期和字符串处理依赖在 pom.xml 里长这样我截了关键几段dependencies !-- Web 层Controller 和内置 Tomcat 都靠它 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis 官方 starter2.x 版本和 Spring Boot 2.x 配套 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.2.2/version /dependency !-- MySQL 8 驱动类名是 com.mysql.cj.jdbc.Driver -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- Lombok实体类不用手写 getter/setter -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这段依赖说明两件事第一项目没有用 MyBatis-Plus意味着所有 SQL 都在 XML 里手写好处是报表统计那种多表关联写起来自由坏处是单表增删改也要自己补第二MySQL 驱动是 8 版本连接串必须带时区参数否则启动时日期字段会差 8 小时这个后面排查章节会细说。如果你想换成 MyBatis-Plus把 starter 换掉、Mapper 继承 BaseMapper 就行但 XML 里已有的自定义 SQL 不用动。2.2 目录结构从 src/main/java 往下拆包包结构是判断一个毕设项目写得规不规范的第一眼印象。这套系统的组织方式是按层拆不是按业务拆好处是新手一眼能看懂坏处是业务一多Service 包会膨胀得厉害。src/main/java/com/example/mes ├── MesApplication.java // 启动类MapperScan 在这里 ├── common │ ├── Result.java // 统一返回体 {code, msg, data} │ ├── BizException.java // 业务异常 │ └── GlobalExceptionHandler.java // RestControllerAdvice 全局兜底 ├── config │ ├── WebConfig.java // 注册拦截器、跨域 │ └── MybatisConfig.java // 分页插件等 ├── controller │ ├── ProdOrderController.java // 生产工单 │ ├── ProdStockController.java // 库存 │ ├── ProdMaterialController.java │ └── SysUserController.java ├── service │ ├── impl │ └── ... ├── mapper │ ├── ProdOrderMapper.java │ └── xml/ // 部分项目把 XML 放 resources 下 ├── entity │ ├── ProdOrder.java │ └── SysUser.java └── utils ├── JwtUtil.java └── UserContext.java // ThreadLocal 存当前登录用户resources下面通常还有mapper/*.xml、application.yml、static或templates以及一份sql/init.sql建库脚本。这份 init.sql 是整个项目能不能跑起来的关键很多压缩包里的 SQL 是作者边开发边导出的缺字段、少索引、甚至漏了初始管理员账号的情况我都遇到过。2.3 配置与依赖application.yml 里真正要改的只有四处配置文件是新手最容易翻车的地方。原作者的本地配置会原封不动留在里面直接启动必然连不上数据库。server: port: 8080 # 被占用就改这里别去改代码 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mes_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 # 改成你自己的 servlet: multipart: max-file-size: 10MB # 物料图片、附件上传上限 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true # 下划线字段自动映射驼峰属性四个必改点端口、数据库地址、账号密码、时区。map-underscore-to-camel-case这一项建议保留为 true否则order_no这种字段映射到orderNo时会拿到 null排查起来很费时间。如果你的 MySQL 是 8.0 且用了默认的caching_sha2_password加密方式老版本驱动会报认证失败要么升级驱动要么把用户改成mysql_native_password这是血泪经验。3. 核心业务链路一张生产工单从创建到扣减库存的完整实现生产管理系统的核心不是增删改查是“工单”这条主线。计划员下达生产计划系统生成工单工单开工前要按 BOM 领料领料就得扣库存扣库存就得防超卖最后工单完工入库、写流水、进报表。这条链路串起来整个项目的数据模型就清楚了。3.1 表设计工单、物料、库存三张主线表先看建表字段名我按常见写法还原实际项目里可能多几个审计字段。-- 生产工单主表 CREATE TABLE prod_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 工单号业务唯一, product_id BIGINT NOT NULL COMMENT 产品 ID, plan_qty INT NOT NULL COMMENT 计划数量, finish_qty INT DEFAULT 0 COMMENT 完工数量, status TINYINT DEFAULT 0 COMMENT 0待生产 1生产中 2已完工 3已取消, plan_start DATE COMMENT 计划开工日, remark VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no) ) COMMENT 生产工单; -- 物料主数据 CREATE TABLE prod_material ( id BIGINT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(32) NOT NULL, name VARCHAR(64) NOT NULL, unit VARCHAR(8) COMMENT 单位个/千克/米, UNIQUE KEY uk_code (code) ) COMMENT 物料; -- 库存表一个物料一条记录 CREATE TABLE prod_stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, material_id BIGINT NOT NULL, qty INT NOT NULL DEFAULT 0 COMMENT 当前库存, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_material (material_id) ) COMMENT 库存; -- 出入库流水所有变动都往这里写一条方便对账 CREATE TABLE prod_stock_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, material_id BIGINT NOT NULL, order_id BIGINT COMMENT 关联工单, change_qty INT NOT NULL COMMENT 正数入库负数出库, type TINYINT COMMENT 1领料 2入库 3盘点, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 库存流水;prod_stock上的uk_material唯一索引很重要它保证一个物料只有一条库存记录后面扣减时用条件更新才不会出现一物料多行、扣了这行漏那行的问题。prod_stock_record单独一张流水表是这套设计的亮点很多粗糙的毕设只改库存不记流水出了账实不符根本查不出原因。3.2 工单创建接口Controller 到 Service 的完整链路Controller 层我建议写薄一点只做参数校验和转发业务逻辑全放 Service。这套源码基本是这个写法算是比较规矩。RestController RequestMapping(/api/order) public class ProdOrderController { Autowired private ProdOrderService prodOrderService; /** * 创建生产工单 * param dto 前端传入的产品、数量、领料明细 * return 新工单主键前端拿去做跳转 */ PostMapping(/create) public ResultLong create(RequestBody Valid OrderCreateDTO dto) { Long orderId prodOrderService.createOrder(dto); return Result.ok(orderId); } }Valid触发 DTO 上的校验注解校验失败会抛MethodArgumentNotValidException由全局异常处理器统一转成{code:500, msg:产品不能为空}这种返回。DTO 长这样Data public class OrderCreateDTO { NotNull(message 产品不能为空) private Long productId; NotNull(message 计划数量不能为空) Min(value 1, message 计划数量至少为 1) private Integer planQty; private String remark; /** 领料明细不传则默认按 BOM 展开这里简化为前端传入 */ NotEmpty(message 领料明细不能为空) private ListMaterialItem materials; }真正的重头在 Service工单号生成、主表落库、库存扣减、流水记录四件事必须在同一个事务里。3.3 库存扣减与事务条件更新才是防超卖的后悔药先看代码再解释为什么这么写。Service public class ProdOrderServiceImpl implements ProdOrderService { Autowired private ProdOrderMapper orderMapper; Autowired private ProdStockMapper stockMapper; Autowired private ProdStockRecordMapper recordMapper; Override Transactional(rollbackFor Exception.class) public Long createOrder(OrderCreateDTO dto) { // 1. 生成工单号PO 日期 4 位当日序列 String orderNo PO LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) String.format(%04d, orderMapper.nextTodaySeq()); // 2. 工单主表落库初始状态 0 待生产 ProdOrder order new ProdOrder(); order.setOrderNo(orderNo); order.setProductId(dto.getProductId()); order.setPlanQty(dto.getPlanQty()); order.setStatus(0); orderMapper.insert(order); // 3. 逐条扣减物料库存 for (MaterialItem item : dto.getMaterials()) { // 条件更新qty 扣减量 才允许扣返回 0 说明库存不足 int affected stockMapper.deduct(item.getMaterialId(), item.getQty()); if (affected 0) { // 抛异常触发整体回滚前面的工单也不会留下脏数据 throw new BizException(物料 item.getMaterialId() 库存不足); } recordMapper.insert(buildRecord(order.getId(), item)); } return order.getId(); } }对应的 Mapper XMLupdate iddeduct UPDATE prod_stock SET qty qty - #{qty} WHERE material_id #{materialId} AND qty #{qty} /update这段代码有两个关键点。第一Transactional(rollbackFor Exception.class)里的rollbackFor不能省Spring 默认只对RuntimeException回滚BizException如果继承的是Exception而不是RuntimeException不加这个参数就会出现“抛了异常但库存已经扣了”的黑匣子现象。第二扣减用WHERE qty #{qty}条件更新而不是先select查库存再判断再update。后者在两个人同时下单时会翻车都查到库存 10都判断够都执行扣减最后库存变负数。条件更新靠数据库行锁保证原子性影响行数为 0 就代表库存不够这个是并发场景下最省事的做法。nextTodaySeq()这个方法在源码里可能是SELECT COUNT(*) 1 FROM prod_order WHERE DATE(create_time) CURDATE()单机低并发够用但严格来说并发下会拿到重复序号被唯一索引拦住。我一般会加一张sys_seq序列表或者直接用 Redis 的INCR按天做 key。3.4 报表统计ECharts 的数据从哪来生产管理系统首页一般挂两张图近 7 天产量趋势和工单状态分布。数据不需要单独建统计表直接从工单表聚合。-- 近 7 天完工数量按天分组 SELECT DATE(create_time) AS day, SUM(finish_qty) AS total FROM prod_order WHERE status 2 AND create_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(create_time) ORDER BY day;Mapper 接口用ListMapString, Object接Service 原样返回前端 ECharts 直接消费// 假设接口返回 [{day:2024-05-01, total:120}, ...] const days res.data.map(i i.day) const totals res.data.map(i i.total) myChart.setOption({ xAxis: { type: category, data: days }, yAxis: { type: value }, series: [{ type: line, data: totals, smooth: true }] })注意SUM(finish_qty)在没有完工记录的日期会返回 null折线图会断掉。稳妥做法是在 Service 里补齐 7 天日期缺的那天填 0。这种小细节不影响跑通但影响答辩演示时的观感属于典型的“能跑”和“好看”之间的差距。4. 登录与权限JWT 拦截器 菜单树的三层控制毕设项目里权限这块最容易写成摆设登录能进菜单能点但接口谁都能调。这套源码用的是 JWT 加拦截器的方案比 Shiro 轻改起来也直观。4.1 登录流程与 Token 生成登录接口做三件事查用户、比对密码、签发 Token 并返回菜单。PostMapping(/login) public ResultLoginVO login(RequestBody LoginDTO dto) { SysUser user userMapper.selectByUsername(dto.getUsername()); // 密码用 BCrypt 存的不能直接 equals if (user null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) { throw new BizException(用户名或密码错误); } String token jwtUtil.createToken(user.getId(), user.getUsername()); LoginVO vo new LoginVO(); vo.setToken(token); vo.setUsername(user.getUsername()); // 菜单树按用户角色查出来前端拿去动态渲染侧边栏 vo.setMenus(menuMapper.selectMenusByUserId(user.getId())); return Result.ok(vo); }passwordEncoder是 Spring Security 的BCryptPasswordEncoder也可以自己引一个 BCrypt 工具类效果一样。Token 里我只放userId和username不放角色和权限原因是 Token 一旦签发就没法改把权限塞进去等于把权限写死了后面改角色得等 Token 过期才生效。权限每次请求查一次缓存灵活得多。4.2 拦截器接口鉴权的最后一道闸public class AuthInterceptor implements HandlerInterceptor { Autowired private JwtUtil jwtUtil; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 放行预检请求否则前端跨域时 OPTIONS 会被拦 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (StringUtils.isBlank(token) || !jwtUtil.verify(token)) { throw new BizException(401, 登录已过期请重新登录); } // 用户信息放 ThreadLocalService 层随取随用 UserContext.set(jwtUtil.getUserId(token)); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 线程池会复用线程不清理会导致下个请求拿到上个用户 UserContext.clear(); } }注册拦截器时一定要excludePathPatterns(/api/login, /error)把登录接口和错误页排除掉。我见过一个版本没排除登录接口结果登录请求先被拦截器拦下抛 401前端收到 401 又跳登录页形成死循环浏览器控制台刷屏这就是典型的配置翻车。4.3 前端路由守卫只做展示不做安全Vue 版本的项目里通常有这么一段router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) return next() if (!token) return next(/login) // 菜单权限只控制侧边栏显示真正的接口权限仍在后端拦截器 next() })这里要建立一个认知前端路由守卫是体验层的东西能绕过不能当安全边界。真正的权限校验必须在后端每个接口上做。这套源码在这一点上做得还行拦截器统一校验 Token菜单表控制侧边栏两边对得上。如果你要加细粒度的按钮权限常见做法是在菜单表里加一个perms字段存权限标识登录时一并返回前端用自定义指令v-hasPerm控制按钮显隐后端再加一层注解校验。5. 本地跑通与排查环境、初始化 SQL 和五个高频翻车点前面讲的是结构和代码这一章说怎么让它真的跑起来。我按自己部署的顺序列照着走一般半小时内能见到登录页。5.1 环境准备与启动顺序先把环境对齐JDK 8 或 11Spring Boot 2.x 别上 17部分依赖会报模块化错误、Maven 3.6、MySQL 5.7 或 8.0、Node 14如果前端是 Vue 独立工程。启动顺序是建库导 SQL、改配置、mvn clean package或直接跑启动类、前端npm install npm run serve。# 1. 建库并导入初始化脚本 mysql -uroot -p -e CREATE DATABASE mes_db DEFAULT CHARSET utf8mb4; mysql -uroot -p mes_db sql/init.sql # 2. 后端打包-DskipTests 跳过测试毕设项目的测试多半跑不过 mvn clean package -DskipTests # 3. 启动也可以直接在 IDE 里跑 MesApplication java -jar target/mes-0.0.1-SNAPSHOT.jar前端如果是独立工程注意vue.config.js里的代理要指向后端端口否则页面能开但所有接口 404。5.2 初始化 SQL 的三个检查点导入脚本后别急着启动先确认三件事。第一管理员账号在不在sys_user表里至少得有一条记录密码是 BCrypt 加密后的字符串不是明文。第二菜单表的parent_id层级对不对根菜单的parent_id应该是 0如果作者导出时写成 null侧边栏会整个渲染不出来。第三自增主键的起始值有些脚本里带着AUTO_INCREMENT100之类导入后新增数据主键从 100 开始不影响功能但看着别扭。5.3 五个高频翻车点现象、原因、解决现象一启动报Access denied for user rootlocalhost。原因是 application.yml 里的密码还是作者的。解决是把spring.datasource.username/password改成自己本机的改完重启。如果密码对但仍报错检查 MySQL 8 的认证插件必要时执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 新密码;。现象二登录成功但所有接口返回 401。原因是拦截器把登录接口也拦了或者前端请求头没带Authorization。先看浏览器 Network 里请求头有没有这个字段再看WebConfig里有没有excludePathPatterns。前端 axios 一般要在请求拦截器里统一加config.headers[Authorization] localStorage.getItem(token)。现象三日期字段查出来差 8 小时。原因是 JDBC 连接串没带serverTimezoneAsia/Shanghai驱动按 UTC 解析。在 url 后面补上这个参数即可已经入库的旧数据要么手动修要么认了。这是 MySQL 8 驱动最常见的坑十个项目里能遇到三个。现象四工单号重复插入报唯一索引冲突。原因是工单号用COUNT(*)1生成并发或者删除过数据后会重号。临时解法是把生成逻辑换成“日期 主键回填”长期解法是单独建序列表或者用 Redis 自增。毕设演示单人的话前一种够用。现象五前端页面白屏控制台报Uncaught SyntaxError: Unexpected token 。原因是静态资源路径没配对请求 JS 文件时被后端当成接口返回了 HTML。检查vue.config.js的publicPath和后端有没有配static目录映射前后端分离的项目里这个错误基本都出在代理配置上。提示排查顺序建议固定为“先看后端启动日志有没有异常 → 再看浏览器 Network 里接口状态码 → 最后看数据库里数据对不对”从下往上找比从上往下猜快得多。6. 二次开发把毕设改成一个能讲清楚的项目这套源码拿来直接交大概率会和别人的撞车。真正有价值的是在它基础上加一个自己的模块面试时能讲清楚“我为什么这么设计”。最省事的切入点是给工单加状态机。原项目里status就是个 TinyInt0 到 3 随便改没有流转约束。你可以建一张prod_order_status_log表记录每次状态变更Service 里加一个changeStatus(orderId, from, to)方法用UPDATE ... WHERE id ? AND status ?做乐观校验改失败就说明状态已经被别人动过。这一个小改动能同时体现事务、并发控制和审计日志三个考点。Transactional(rollbackFor Exception.class) public void changeStatus(Long orderId, int from, int to) { int affected orderMapper.updateStatus(orderId, from, to); if (affected 0) { throw new BizException(工单状态已变更请刷新后重试); } statusLogMapper.insert(new StatusLog(orderId, from, to, UserContext.get())); }第二个改造方向是把报表从“查全表聚合”改成定时任务预计算。加一张prod_daily_stat表用Scheduled(cron 0 0 1 * * ?)每天凌晨跑一次把前一天的产量、不良数、工时算好存进去报表接口直接查这张表。数据量小的时候看不出差别但这是从“学生作业”往“工程做法”迈的一步。第三个方向是补单元测试。毕设项目里测试目录基本是空的你挑库存扣减这个最核心的方法写两个用例库存充足时扣减成功、库存不足时抛异常且工单没落库。用SpringBootTest加Transactional让每个用例跑完自动回滚不污染数据库。验证方法很简单改完之后把工单创建接口用 Postman 并发打 50 次看库存会不会变负、工单号会不会重。这个压测我自己每次改完扣减逻辑都跑一遍比看代码可靠。从那以后我每次接手这类毕设源码都强制自己先跑通登录、再跑通一条完整的工单链路、最后才动代码顺序反了一定会在某个 401 或者空指针上耗掉半天。这套 java生产管理系统 的骨架是完整的缺的只是把它从“能演示”推到“能讲清楚”的那几步源码包里这些模块都是现成的下载下来按目录对一遍比自己从零搭省太多时间。希望帮到你。本文还有配套的精品资源点击获取