☰
Spring Boot+Vue生产管理系统源码:工单报工库存与二次开发
2026/11/4 13:38:47 网站建设 项目流程

简介:这是一套面向制造型企业信息化建设与二次开发学习者的生产管理系统源代码,围绕生产计划、物料需求、库存出入库、订单跟踪、质量控制和报表分析等核心业务展开,适合具备一定编程与数据库基础、希望理解中小型生产管理流程及ASP系统实现的开发者参考。压缩包共165个文件,约1.33MB,以97个ASP服务端页面为主体,配合16个JavaScript脚本、9个CSS样式、图片资源以及数据库文件,覆盖前台界面、入库出库、物料领用和员工信息等业务页面,能较完整地呈现系统结构。已有2009人学习下载,读者可据此梳理功能模块划分、数据表关系与权限控制思路,并进一步研究生产计划与库存联动的实现方式,在此基础上完成功能裁剪、流程调整或二次开发,用于课程设计、毕业项目及企业内部原型验证。

1. 从车间主任的七张 Excel 说起:这套生产管理系统源代码到底解决什么问题

车间主任抱着一台笔记本找到我,桌面上摊着七个 Excel:排产表、领料单、报工记录、库存台账、质检汇总、设备点检、交货计划。每天下班前他都要花两个小时把这些表手工核对一遍,数据对不上时只能凭记忆猜哪张表更新。这套生产管理系统源代码要解决的就是这件事:把工单、物料、报工、库存、质检串成一条有状态、可追溯的数据链,让人从反复核对表格的工作里解放出来。

它更适合两类人。一类是厂里负责信息化、手上有一个车间要管、但预算买不起成套商业软件的技术负责人,把源码拉下来改改车间和物料主数据就能上线;另一类是手上已经拿到源码、想照着学一套完整业务系统怎么分层、怎么做事务和状态流转的开发者。系统体量不算大,但业务闭环是全的,从下单、排产、领料到报工、入库、结算一条线走到底,每个环节都有能动手改造的地方。

2. 技术栈与目录结构:先摸清骨架再动手

拿到一套陌生源码,我一般不急着跑,先花二十分钟看三样东西:依赖清单、目录树、建表脚本。这三样看完,项目大概是什么水平、坑会出在哪,心里就有数了。这套生产管理系统走的是国内工厂信息化最常见的技术路线,没有追新,也没有为了架构而架构,取舍逻辑很清楚。

2.1 技术选型的取舍:为什么是 Spring Boot + Vue 这套组合

车间机房的部署条件通常是这样的:一台 Windows Server、内网、没有外网、运维就一两个人。任何需要容器编排、需要多个中间件的方案,落地成本都会翻倍。所以系统选的是「一个 jar 包 + 一堆静态文件」就能跑起来的形态,前面挂个 Nginx 或者直接用内置容器对外提供服务。

层次选型在这套系统里承担什么
后端框架Spring Boot 3.xREST 接口、声明式事务、定时任务、全局异常处理
持久层MyBatis-Plus单表增删改查省掉模板代码,复杂报表仍手写 XML
数据库MySQL 8工单、库存、报工全部落库,一致性靠事务和唯一索引兜底
缓存Redis登录态、数据字典、工单号发号器
前端Vue 3 + Element Plus + Vite车间看板和 PC 端共用一套组件库
鉴权Spring Security + JWT无状态鉴权,后面接 PDA 扫码枪不用改后端

值得注意的是持久层的用法:系统并没有把 MyBatis-Plus 用到极致,像库存扣减、报表汇总这类必须精确控制的 SQL,全部写在 XML 里手写,只有主数据维护这种纯 CRUD 才用条件构造器。常见做法就是这样分工:能自动生成的绝不手写,涉及并发和金额的一律手写并配注释。

至于为什么不上微服务,我的判断很直接:这套系统最终的并发量是几十个工人同时扫码,单机 MySQL 加一层 Redis 完全够用。拆成微服务之后,一个报工动作要跨工单服务、库存服务、消息队列三处,分布式事务的复杂度会立刻吃掉所有收益,而厂里的运维根本维护不了五六个服务实例。

2.2 目录结构:模块边界基本就是事务边界

后端的模块划分是这个项目里我最认可的部分。很多同类源码所有代码堆在src/main/java下按 controller、service、mapper 分三层,改一个需求要跳十几个文件;这套拆分方式按职责切块,找代码的路径短很多。

production-mes/ ├── mes-admin/ # 启动模块:Application 主类、Controller、全局配置 ├── mes-common/ # 通用返回体 R、业务异常、常量、日期工具、枚举基类 ├── mes-domain/ # 实体 Entity、入参 DTO、出参 VO、状态枚举 ├── mes-service/ # 业务实现,所有 @Transactional 都落在这一层 ├── mes-dao/ # Mapper 接口 + resources/mapper 下的 XML ├── mes-web/ # Vue3 前端:views / api / router / store ├── sql/ # 01-schema.sql 建表、02-data.sql 初始化数据 └── docker-compose.yml # 本地一键起 MySQL 和 Redis

这个结构最重要的价值在于:mes-service是唯一允许写事务注解的模块,mes-dao只负责取数,不带任何业务判断。这样一来,「报工会不会把库存扣重复」这类问题只需要在一个文件里看,不用满项目找。我接手类似项目时,第一件事就是看事务注解有没有漏到 Controller 层去,一旦漏上去,跨方法调用时事务基本等于没有。

依赖方向也是单向的:admin 依赖 service,service 依赖 dao 和 domain,common 被所有模块依赖。父 pom 里统一管理版本号,子模块之间禁止出现循环依赖。这一点在二次开发加新模块时非常关键,后面第六章还会提到。

2.3 数据模型:六张主表撑起整条链路

判断一个生产管理系统是不是真做过,看它的表设计就够了。只设计工单表和物料表的是 Demo,能把报工流水和库存流水拆开、把余额和明细分开的才是能上线的设计。这套源码的核心业务表有六张,外加权限相关的几张系统表。

表名作用关键设计点
prod_work_order生产工单主表wo_no唯一索引、status状态机、version乐观锁
prod_work_order_item工单用料明细记录 BOM 展开后的理论用量与实际用量
prod_report报工记录只增不改,每条都有操作人和时间段
prod_stock库存余额(material_id, warehouse_id)唯一,qty为可用量
prod_stock_log库存流水每笔出入库一条,含来源单据号
qc_record质检记录关联报工记录,支持不良品数量登记

工单表的建表语句值得单独看一下,几个字段是后面所有逻辑的地基:

CREATE TABLE prod_work_order ( id BIGINT NOT NULL AUTO_INCREMENT, wo_no VARCHAR(32) NOT NULL COMMENT '工单号,规则 WO+yyyyMMdd+4位流水', material_id BIGINT NOT NULL COMMENT '产成品物料ID', plan_qty DECIMAL(18,4) NOT NULL COMMENT '计划数量', done_qty DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '已报工数量', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待排产 1已排产 2生产中 3已完工 4已关闭', plan_start DATETIME NULL COMMENT '计划开工时间', workshop_id BIGINT NULL COMMENT '车间/产线ID', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_wo_no (wo_no), KEY idx_status_plan (status, plan_start) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='生产工单';

几个细节的用意:plan_qty和done_qty用DECIMAL(18,4)而不是INT,因为化工、食品这类行业会按吨、按千克报工,小数位必须留够;status用TINYINT而不是字符串,是为了让idx_status_plan这个联合索引在筛选「待排产并按计划时间排序」时能真正生效;version字段是后面报工并发扣库存的关键,没有它,两个人同时点报工就会把库存扣成负数。

库存表的设计更简单,但约束更严格:

CREATE TABLE prod_stock ( id BIGINT NOT NULL AUTO_INCREMENT, material_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, qty DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '可用库存', version INT NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_mat_wh (material_id, warehouse_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存余额';

uk_mat_wh这个唯一索引不是可有可无的装饰。少了它,「同一个物料在同一个仓库出现两条余额记录」这种脏数据会以各种意想不到的方式冒出来,比如重复入库时插入而非更新。有它在,任何一次错误的 insert 都会当场报错,问题暴露在写入点而不是三个月后的盘点。

3. 环境搭建与启动:从零跑起来的最小路径

我这几年接手过不少这种「下载下来就是一堆文件夹」的源码,最费时间的往往不是读代码,而是让它第一次成功启动。这一章按顺序记录我实际跑通它的完整过程,包括每一步验证什么。如果你的环境和我不一样,比如 MySQL 版本不同或者端口被占用,也能从每步的输出里快速定位。

3.1 依赖版本与环境准备

先把版本对一遍。Spring Boot 3 要求 JDK 17 以上,这一点必须提前确认,用 JDK 8 会直接编译不过,报的错还特别不直观。前端 Vue 3 配 Vite,Node 建议 18 或 20 的 LTS 版本。

组件建议版本用途
JDK17+后端编译运行
Maven3.8+多模块构建
MySQL8.0+主数据库
Redis6.0+登录态与发号器
Node.js18 / 20 LTS前端构建

项目根目录带了一个docker-compose.yml,如果本机装了 Docker,最简单的方式是直接起中间件,省去装 MySQL 和 Redis 的功夫:

# 只启动数据库和缓存,后端仍在本地 IDE 里跑,方便断点调试 docker compose up -d mysql redis # 确认容器状态,mysql 显示 healthy 再继续 docker compose ps

不装 Docker 也行,本地装好 MySQL 8 和 Redis,记住端口和密码,下一步改配置文件。这一步我通常会顺手验证一下 Redis 是否可连,redis-cli ping返回PONG就对了——后面登录功能依赖它,启动时连不上不会报致命错误,但登录会一直失败,排查起来很绕。

3.2 建库、导数据与改配置文件

数据库脚本在sql/目录下,顺序不能颠倒:先 schema 后 data。初始化数据里包含一个管理员账号、两个车间、若干物料主数据和一份示例 BOM,够跑通完整流程。

# 建库(字符集必须是 utf8mb4,物料名称里常有生僻字) mysql -uroot -p -e "CREATE DATABASE mes_prod DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;" # 导入建表脚本和初始化数据 mysql -uroot -p mes_prod < sql/01-schema.sql mysql -uroot -p mes_prod < sql/02-data.sql # 验证:工单表应有数据,状态字段分布正常 mysql -uroot -p mes_prod -e "SELECT status, COUNT(*) FROM prod_work_order GROUP BY status;"

接着改后端配置文件。关键参数我列在下面的片段里,注释写清了每个值怎么改:

server: port: 8080 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/mes_prod?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 你的密码 # 改成自己的 hikari: maximum-pool-size: 20 connection-timeout: 30000 data: redis: host: 127.0.0.1 port: 6379 database: 0 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mes: jwt: secret: 换成你自己的随机字符串 # 至少 32 位,线上不要用默认值 expire-minutes: 120 workorder: over-ratio: 1.02 # 允许超报 2%,报工校验会用到

serverTimezone=Asia/Shanghai这个参数千万别省。MySQL 8 驱动默认按 UTC 解析时间,省掉之后你会看到「计划开工时间 08:00,列表里显示 00:00」这种诡异现象,而且只影响时间字段,很容易误判成前端的问题。

3.3 前后端启动与三分钟自检

后端在根目录执行mvn clean install -DskipTests,先确认多模块全部编译通过。然后进mes-admin模块,跑主类或者mvn spring-boot:run。控制台看到 Tomcat started on port 8080 就算起来了。

前端部分:

cd mes-web npm install --registry=https://registry.npmmirror.com # 内网或网络慢时换源 npm run dev # 如果前端和后端不在同一台机器,改 vite.config.js 里的代理目标 # server: { proxy: { '/api': { target: 'http://127.0.0.1:8080', changeOrigin: true } } }

起来之后别急着点页面,先按下面三步自检,能省掉 80% 的「页面白屏」问题:

第一,直接访问http://127.0.0.1:8080/actuator/health(如果引入了 actuator)或随便一个公开接口,确认后端真的对外服务,而不是端口被占用后启动到了别的端口。第二,用初始化数据里的管理员账号登录,看返回的 token 是不是正常签发的,如果登录接口 500,大概率是 Redis 连不上或者表里密码加密方式不匹配。第三,进工单列表页,确认分页能翻到第二页——只测第一页测不出排序和计数的问题。

提示:前端npm run dev默认监听 5173,如果页面上接口全部 404,先看浏览器 Network 里的请求地址有没有被代理到 8080,再看后端有没有配 context-path。

4. 核心链路拆解:一张工单从下单到入库走完的代码路径

业务系统最值得读的部分永远是主流程。这套系统里,一张工单的生命周期是「创建 → 排产 → 领料 → 报工 → 质检 → 入库 → 关闭」,其中代码密度最高、最容易出问题的是创建、报工和状态流转三处。把这三段读懂,剩下的模块基本是同一套写法。

4.1 工单创建:发号、校验与 BOM 展开

工单号不能靠数据库自增,因为业务上要求它带日期且可读。系统用 Redis 的INCR按天做流水,key 带上日期前缀,天然实现「每天从 0001 开始」。

@Service @RequiredArgsConstructor public class WorkOrderServiceImpl implements WorkOrderService { private final WorkOrderMapper workOrderMapper; private final WorkOrderItemMapper itemMapper; private final BomMapper bomMapper; private final StringRedisTemplate redisTemplate; /** * 创建工单:生成唯一工单号 -> 按 BOM 展开用料明细 -> 落库 * 注意:工单号和工单必须是同一事务,否则会出现号被消费但工单没落库的空号 */ @Override @Transactional(rollbackFor = Exception.class) public Long create(WorkOrderCreateDTO dto) { // 1. 工单号:WO + 日期 + 4 位日内流水,key 设置 25 小时过期兜底 String day = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); String key = "mes:wo:seq:" + day; Long seq = redisTemplate.opsForValue().increment(key); if (seq != null && seq == 1L) { redisTemplate.expire(key, Duration.ofHours(25)); } String woNo = "WO" + day + String.format("%04d", seq); // 2. 主表落库 WorkOrder wo = new WorkOrder(); wo.setWoNo(woNo); wo.setMaterialId(dto.getMaterialId()); wo.setPlanQty(dto.getPlanQty()); wo.setDoneQty(BigDecimal.ZERO); wo.setStatus(WorkOrderStatus.PENDING.getCode()); wo.setPlanStart(dto.getPlanStart()); wo.setWorkshopId(dto.getWorkshopId()); workOrderMapper.insert(wo); // 3. 按 BOM 展开用料,计划量乘以单耗得到需求量 List<Bom> boms = bomMapper.selectByProductId(dto.getMaterialId()); for (Bom bom : boms) { WorkOrderItem item = new WorkOrderItem(); item.setWorkOrderId(wo.getId()); item.setMaterialId(bom.getComponentId()); item.setRequireQty(bom.getUsage().multiply(dto.getPlanQty())); itemMapper.insert(item); } return wo.getId(); } }

increment返回 1 时设置过期时间这一步容易被漏掉。如果只increment不过期,Redis 里会一年攒下几百个 key,虽然不影响功能,但哪天做数据迁移或者缓存清理就会踩到「工单号从 0001 重新开始」然后撞唯一索引的坑。这类问题没什么技术含量,纯粹是习惯问题。

rollbackFor = Exception.class是必须写的。Spring 默认只对RuntimeException回滚,一旦某个校验抛了受检异常,工单主表已经插入、明细没插进去,就留下一条永远排不了产的残缺工单。

4.2 报工:一个事务里的四件事

报工是整个系统里最关键的一次写操作,它同时要写报工记录、累加工单完成量、扣减原料库存、记库存流水。四件事必须原子完成,中间任何一步失败都要整体回滚,否则就会出现「记录有了但库存没扣」这种对不上账的情况。

@Override @Transactional(rollbackFor = Exception.class) public void report(ReportDTO dto) { // 1. 幂等校验:同一个请求号重复提交直接返回,不做二次扣减 if (reportMapper.existsByRequestNo(dto.getRequestNo())) { return; } WorkOrder wo = workOrderMapper.selectById(dto.getWorkOrderId()); if (wo == null) { throw new BizException("工单不存在"); } // 2. 状态校验:只有已排产、生产中两种状态可以报工 if (!WorkOrderStatus.canReport(wo.getStatus())) { throw new BizException("当前工单状态不允许报工"); } // 3. 超报校验:累计数量不能超过计划量 × 允许比例 BigDecimal after = wo.getDoneQty().add(dto.getQty()); BigDecimal limit = wo.getPlanQty().multiply(new BigDecimal("1.02")); if (after.compareTo(limit) > 0) { throw new BizException("累计报工量 " + after + " 已超过上限 " + limit); } // 4. 写报工记录(带 requestNo 唯一索引,数据库层面再兜一次幂等) Report report = new Report(); report.setWorkOrderId(wo.getId()); report.setRequestNo(dto.getRequestNo()); report.setQty(dto.getQty()); report.setWorkerId(dto.getWorkerId()); report.setReportTime(LocalDateTime.now()); reportMapper.insert(report); // 5. 累加工单完成量并推进状态,用乐观锁防止并发覆盖 int rows = workOrderMapper.addDoneQty(wo.getId(), dto.getQty(), wo.getVersion()); if (rows == 0) { throw new BizException("工单已被他人更新,请刷新后重试"); } // 6. 按 BOM 倒扣原料,库存不足会抛异常触发整体回滚 List<WorkOrderItem> items = itemMapper.selectByWorkOrderId(wo.getId()); for (WorkOrderItem item : items) { BigDecimal consume = item.getUnitUsage().multiply(dto.getQty()); stockService.deduct(item.getMaterialId(), dto.getWarehouseId(), consume, report.getId()); } }

对应的两条 SQL 值得对着看:

-- 累加完成量:带 version 条件,更新失败说明有人抢先改了 UPDATE prod_work_order SET done_qty = done_qty + #{qty}, status = CASE WHEN done_qty + #{qty} >= plan_qty THEN 3 ELSE 2 END, version = version + 1 WHERE id = #{id} AND version = #{version}; -- 扣减库存:qty >= 数量 这个条件保证永远不会扣成负数 UPDATE prod_stock SET qty = qty - #{qty}, version = version + 1 WHERE material_id = #{materialId} AND warehouse_id = #{warehouseId} AND qty >= #{qty};

参数说明一下:#{qty}是本次报工数量,#{version}是读取时的版本号;库存那条 SQL 里的qty >= #{qty}不是可有可无的校验,它把「够不够扣」的判断交给了数据库的行锁,比先SELECT再UPDATE可靠得多。返回影响行数为 0 时,服务层统一抛业务异常,让整个事务回滚。这种「条件更新 + 判断影响行数」的写法在库存、账户余额这类场景几乎是标配,比分布式锁轻量得多。

4.3 状态流转:把散落的 if 收进一个枚举

很多同类源码里,工单状态是散落在各个 Service 的if (status == 1)里,改一条规则要全文搜索。这套系统把流转规则收进枚举,canReport、canClose这类判断都从这里出。

@Getter @AllArgsConstructor public enum WorkOrderStatus { PENDING(0, "待排产", EnumSet.of(SCHEDULED_PLACEHOLDER)), SCHEDULED(1, "已排产", EnumSet.of(PRODUCING, PENDING)), PRODUCING(2, "生产中", EnumSet.of(FINISHED)), FINISHED(3, "已完工", EnumSet.of(CLOSED)), CLOSED(4, "已关闭", EnumSet.noneOf(WorkOrderStatus.class)); private final int code; private final String desc; private final Set<WorkOrderStatus> next; public static boolean canReport(Integer code) { return code != null && (code == SCHEDULED.code || code == PRODUCING.code); } }

这个写法不复杂,但收益很明显:新来的人想加一个「暂停」状态,只需要在枚举里补一行和它的后继集合,不用去十几个文件里找 if。我一般还会再加一个transfer(from, to)的静态方法,在 Service 里统一调用,任何非法跳转都在入口就被拒绝,而不是等到数据错乱才发现。

5. 避坑与排查:跑这套系统时最常见的五处问题

以下五条是我在本地和测试环境真实遇到过的,按「现象 → 原因 → 解决」记录。有些是项目自身的问题,有些是环境导致的,但都会卡住第一次跑通。

5.1 环境与配置类问题

现象一:启动直接失败,报The server time zone value '中国标准时间' is unrecognized。原因是 MySQL 8 的驱动要求连接串显式指定时区,而配置里只写了数据库地址。解决办法是在spring.datasource.url后面补上serverTimezone=Asia/Shanghai。如果补了还报,检查是不是 IDE 里跑的时候用了另一份application-dev.yml覆盖了主配置,多环境配置文件同时存在时,激活的 profile 优先级高于默认文件。

现象二:后端起来了,前端登录一直转圈,Network 里请求是 pending 然后超时。原因多数是跨域或者代理没生效。Vite 的proxy只在开发服务器里生效,如果前端是打包后用 Nginx 托管的,就得在 Nginx 里配location /api/ { proxy_pass http://后端地址:8080/; },而且proxy_pass结尾有没有斜杠会导致路径拼接结果不同。解决方式很直接:先在浏览器里直接访问后端接口地址,能通说明后端没问题,问题一定在代理这一层。

5.2 数据与事务类问题

现象三:并发测试时库存出现负数,日志里没有任何异常。原因是早期版本的扣减逻辑写成了「先查余额、判断够不够、再更新」,两个请求同时查到余额 10,各自扣 8,最后变成 -6。解决的写法就是 4.2 里那条带qty >= #{qty}的条件更新,靠数据库行锁把并发串行化。改完之后记得同时给prod_stock加一条CHECK (qty >= 0)或者保留条件更新中的判断,双保险。

现象四:报工失败了,但工单的完成数量已经涨上去了。这是事务边界的经典问题。原因通常是状态更新用了一个新的事务(比如在另一个 Service 方法上标了REQUIRES_NEW),或者异常被try-catch吞掉没有重新抛出。排查办法是在报工入口打一行日志,把事务 ID 和各个环节的耗时打出来,看是不是同一个事务;解决就是统一在mes-service层的入口方法上加@Transactional(rollbackFor = Exception.class),内部方法不单独开事务。

5.3 业务规则类问题

现象五:工单列表翻到后面几页越来越慢,几万条数据时一次要三四秒。原因是分页查询用了ORDER BY create_time DESC,但索引建的是(status, plan_start)。MySQL 用不上排序索引就只能全表扫再 filesort。解决办法有两个方向:一是把列表默认排序改成走索引的字段组合,二是给排序字段单独建索引。我更倾向第一种,因为工单列表的默认排序本来就该按计划开工时间,而不是创建时间——业务上更合理,性能上也顺带解决了。改完用EXPLAIN看一眼type是不是从ALL变成了range,这个验证动作别省。

注意:EXPLAIN显示的rows是估算值,实际扫描行数以Handler_read_next状态变量为准,两者差距大时说明统计信息过期,ANALYZE TABLE之后再测一遍。

6. 二次开发:从加一个字段到加一道工序

真正落到厂里用,改造是躲不掉的,最常见的两类需求是「加个字段」和「加道工序」。加字段看着简单,实际是最容易漏改的:只改数据库不改 DTO,前端拿不到值;改了 DTO 忘了加参数校验,脏数据就进去了。我一般的做法是照着一张固定清单走,四步走完基本不会漏。

改造点需要动的文件验证方式
工单加「客户订单号」建表脚本 → 实体 → DTO/VO → 前端表单与列表列新建工单后查库、列表接口返回、导出 Excel 都有值
加一道「返修」工序work_order_route表 → 工艺路线配置页 → 报工时的工序校验走完整流程,确认返修单能独立报工且不计入正品数
接入 PDA 扫码报工新增 Controller → 复用ReportService→ JWT 换扫码登录用 Postman 模拟扫码,确认幂等和库存扣减正常

加字段的关键不在 SQL,而在于把「必填校验」和「列表展示」一起做掉。举个实际改法,给工单加客户订单号,实体加字段之后,DTO 上一定要跟一个校验注解,否则前端不填也能提交:

public class WorkOrderCreateDTO { @NotNull(message = "产成品不能为空") private Long materialId; @NotNull @DecimalMin(value = "0.0001", message = "计划数量必须大于 0") private BigDecimal planQty; // 新增:客户订单号,允许为空但要限制长度,防止前端粘贴超长文本 @Size(max = 64, message = "客户订单号不能超过 64 个字符") private String customerOrderNo; }

加字段时有个我踩过的坑:只在实体上加字段、没在 Mapper 的 XML 里补列名,结果单表查询正常,一到联表查询就变成 null,排查半天才发现是手写 SQL 的resultMap没有映射。所以这套系统我改的时候会顺手把resultMap也过一遍,宁可多写一行。

至于加工序,影响面比加字段大得多。工艺路线本身是一张独立表,但报工时要把工序号带上,工单状态的推进也要按工序走完之后才能置为完工。我的建议是先在work_order_item之外单独建work_order_route,不要试图在原表上塞 JSON 字段存工序,短期内省事,后面做工序级报工统计时一定会返工。

改造完怎么验证?我的习惯是准备一份极简回归清单,跑一遍不超过十分钟:新建工单 → 排产 → 报工两次(一次正常、一次超量)→ 查库存流水条数 → 完工入库 → 关闭工单。这五步全过,基本可以认为改造没有破坏主链路。清单写在仓库的docs/smoke-test.md里,谁改谁跑。

从那以后,我每次动这类系统的核心表之前,都会先把这份回归清单跑一遍留存结果,改完再跑一遍做对比,两次结果一致才敢提交。吃过一次「只改了个字段,结果把扣库存的 SQL 一起带崩」的亏之后,这个习惯就再也没断过。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询