简介:这是一份面向Java开发者、智能制造研究者及车间管理信息化从业者的车间调度智能排产集成框架源码,用来解决传统排产方式效率低下、多因素耦合导致调度困难的问题。压缩包内共298个文件,主体为281个Java源文件,覆盖排产算法、实验对比与性能指标等模块,另有13个keep占位文件、2个Markdown说明文档、1个readme.txt以及.gitignore配置,整体仅1.68MB,目录结构清晰,便于深入研读。当前已有830人学习使用。源码包含蚁群-遗传混合算法、蚁群算法等启发式排产策略,以及作业车间置换编码、分布式柔性作业车间最小时间NEH编码、双目标交叉算子等进阶实现,可以帮助读者理解从编码、求解到统计分析的全过程;同时框架预留模块化集成接口,能够与ERP、MES等系统对接,适用于课程设计、毕业设计或生产排产系统的预研与二次开发。
1. 基于Java的车间调度智能排产集成框架:先定模型,再谈算法,最后留出插拔口
车间排产这件事,经常不是卡在算法上,而是卡在“数据模型和算法分不开”。计划员手里Excel改来改去,IT这边写了一个调度勤快的算法,却不清楚设备日历、工序顺序和临时插单在代码里怎么放。基于Java的车间调度智能排产集成框架,核心在于把排产过程变成“可替换的引擎 + 可进化的模型 + 可监控的接口”,而不是把遗传算法在main函数里写一遍然后只跑一次demo。这个框架适合Java后端工程师、MES/ERP实施顾问,以及准备系统设计面试的开发者。在这道高频出现的Java面试系统设计题背后,真正被追问的往往不是算法收敛性,而是你现在要往下读的领域模型边界、算法接口设计和框架如何被业务系统调用。今天这篇按我整理源码骨架的路线,从领域对象、算法接口、Spring Boot集成一直讲到上线前的验证细节。
2. 定义排产对象:用Java类把车间资源、工单和工序变成可计算的领域模型
车间调度最常见的失败原因是把排产核心和业务系统耦合在一起,工单状态一变,排产结果就没法复用。为了避免这种问题,在搭建基于Java的调度框架时,我先建立一套与数据库无关的领域对象。这套对象不再直接对应MES表,而是对应排产算法需要的“原子概念”:一个工单包含多个工序,每个工序占用某个具备特定技能的资源一段时间,最终形成可校验的排产结果。工业界常说的APS排产,本质上就是在这套对象上运行约束与搜索算法。
2.1 核心实体:Job、Operation、Resource与Schedule的字段设计
常见的做法是先画ER图:Job、Operation、Resource三张表,再加一个Schedule结果表。落到Java代码里,我更喜欢用record表达这些高内聚值对象,因为record天然不可变,在遗传算法反复计算适应度时能安全地在线程池中传递,减少意外修改造成的结果不确定性。下面是一组可以直接放进源码工程的最小模型:
public record Job( String id, int releaseDate, // 最早可开工时间 int dueDate // 约定交期,用于计算拖期惩罚 ) {} public record Operation( String id, String jobId, int seq, // 工序序号 String requiredSkill, // 需要的技能/物料组 int duration, // 标准工时,单位:分钟 List<String> predecessors // 前序工序ID列表 ) {} public record Resource( String id, String name, Set<String> skills, // 支持哪些技能 List<TimeSlot> calendar // 可用时间段,按分钟表示 ) {} public record TimeSlot(int start, int end) { public boolean contains(int s, int e) { return start <= s && e <= end; } } public record Assignment(String operationId, String resourceId, int startTime, int endTime) {} public record Schedule(List<Assignment> assignments) {}这里的核心参数需要解释清楚:时间全部改成相对开工日的整数分钟,比如第1天8:30转化为510分钟。这样可以避免LocalDateTime在算法迭代中频繁创建造成的性能损耗,同时便于做两个作业之间的前后顺序比较。seq和predecessors同时出现,是为了兼容两种工艺表达方式:流水车间用seq顺序即可,作业车间有分叉和汇合时用predecessors更稳妥。Resource.calendar不是一张排班表,而是可用工时的合并集合,设备检修、节假日都可以提前剔除,避免算法在不可用时段上浪费迭代。
| 字段 | 典型类型 | 设计要点 |
|---|---|---|
| Job.releaseDate | int | 影响调度最早开始时间,单位必须是分钟 |
| Operation.requiredSkill | String | 决定资源候选集,比直接绑定resourceId更柔性 |
| Resource.calendar | List | 用区间合并算法压缩后,搜索速度更快 |
| Assignment.startTime | int | 不存DateTime,是为了快速检测资源冲突 |
很多刚接手排产源码的Java工程师会把工序直接绑定到某个设备,看起来简单,但一旦设备故障需要替代资源,整个约束就要重写。把技能抽出来当中间层,这正是集成框架与一次性的排产小工具本质差异。
2.2 约束条件从哪里进入模型:前置关系、资源互斥和生产日历的代码投影
模型定义好之后,接下去要把“口头上的车间规矩”翻译成可计算的约束代码。我一般不是把校验逻辑散落在算法各分支里,而是集中放到一个ScheduleValidator中。这样算法无论怎么更换,底层硬约束都不会被绕过。下面是常见做法的最小实现:
public class ScheduleValidator { public boolean validate(Schedule schedule, SchedulingProblem problem) { Map<String, Operation> opMap = problem.operations(); for (Assignment a : schedule.assignments()) { Operation op = opMap.get(a.operationId()); for (String preId : op.predecessors()) { Assignment pre = findAssignment(schedule, preId); if (pre == null || pre.endTime() > a.startTime()) { return false; // 前工序未完成,不能开工 } } } Map<String, List<Assignment>> byResource = schedule.assignments().stream() .collect(Collectors.groupingBy(Assignment::resourceId)); for (List<Assignment> list : byResource.values()) { list.sort(Comparator.comparingInt(Assignment::startTime)); for (int i = 1; i < list.size(); i++) { if (list.get(i).startTime() < list.get(i - 1).endTime()) { return false; // 同一资源在同一时刻只允许一个工序 } } } for (Assignment a : schedule.assignments()) { Resource r = problem.resourceMap().get(a.resourceId()); boolean timeOk = r.calendar().stream() .anyMatch(ts -> ts.contains(a.startTime(), a.endTime())); if (!timeOk) { return false; // 排到了设备休息时间上 } } return true; } }这个类里的三个for循环分别对应车间管理里最常被问到的三类硬约束。第一段在严格检查工序依赖,适合所有复杂装配关系;第二段用分组排序检查资源是否被抢占;第三段把设备日历包装成区间集合,让日历判断从“逐分钟扫描”降到区间查询。后端的候选资源过滤条件里写明了技能匹配和前置完成时间要求,分配到最早可用的时间间隙,这种“贪心+局部最早空闲”的方法通常能覆盖60%以上的中小企业车间调度需求。
一旦约束数量增多,建议把ScheduleValidator抽象成接口,把每个约束单独成一个类,例如PrecedenceConstraint、ResourceCapacityConstraint、CalendarConstraint。这样新增一个制约因素,不需要动算法主体,只要在Validate阶段增加一个实现类。这也是标题里“集成框架”而不是“单独排产程序”的原因:框架能让外部约束变成可插拔扩展点,而不是某一版需求写死在代码里。
3. 智能排产引擎实现:规则调度与遗传算法共用一套SchedulingAlgorithm接口
有了领域模型,接下来需要回答“智能排产怎么放进Java工程”。我的做法是先把算法入口抽象成SchedulingAlgorithm接口,然后同时实现基于规则的调度器和基于遗传算法的调度器,用配置决定运行时使用哪一种。这样做有两个直接好处:小批量车间用规则调度即可,中大型离散制造再切到遗传算法;生成初始解时也能用规则调度快速给遗传算法提供优质种子,而不是让它自己去随机摸索。
3.1 规则调度:用EDD、SPT等启发式生成可行解
规则调度是最容易在企业里落地的算法,它不是严格意义上的“智能”,但它利用EDD(最早交货期)、SPT(最短加工时间)等策略,在毫秒级时间内给出一个可行程度很高的解。我一般用它作为系统的兜底算法,如果MES数据质量差或者算法参数没有调优,至少能保证当天排产结果不至于颗粒度为零。实现时只要在可开工工序集合上做多重排序:
public class RuleBasedScheduler implements SchedulingAlgorithm { @Override public Schedule solve(SchedulingProblem problem) { List<Operation> ready = findReadyOperations(problem); ready.sort(Comparator .comparingInt((Operation op) -> dueDateOf(problem, op)) .thenComparingInt(Operation::duration)); for (Operation op : ready) { List<Resource> candidates = problem.resources().stream() .filter(r -> r.skills().contains(op.requiredSkill())) .filter(r -> isIdleAfterPredecessor(problem, op, r)) .toList(); if (!candidates.isEmpty()) { assignToEarliestResource(op, candidates, problem); } } return snapshot(problem); } }代码逻辑是先筛出当前所有前置工序已经完成的工序,再按“交货期近的优先、交期相同则工时短先做”排序。真正排产时,这个排序键就是一个策略参数,切换成SPT就改成Comparator.comparingInt(Operation::duration)。候选资源过滤条件里写明了技能匹配和前置完成时间要求,分配到最早可用的时间间隙,这种“贪心+局部最早空闲”的方法通常已经能覆盖60%以上的中小企业车间调度需求。
| 规则组合 | 排序主键 | 排序次键 | 适用场景 |
|---|---|---|---|
| EDD → SPT | dueDate | duration | 交期压力大的多品种小批量 |
| SPT → 资源均衡 | duration | resourceLoad | 瓶颈设备产能紧张 |
| LPT → EDD | duration倒序 | dueDate | 长工序集中安排在可用窗口 |
这一层设计的重点是把规则本身写成可组合的比较器,而不是在for循环里写if else。后续想在框架里增加FIFO、LS等新规则,只增加新的Comparator即可,不需要重排整个类。
3.2 遗传算法排产:编码、适应度函数与关键参数设置
规则调度会陷入局部最优,特别是工单数量超过50个、瓶颈设备多于3台时,排产结果的设备空闲碎片会明显变多。工业界智能排产的核心优化手段是遗传算法(GA)。在Java工程里,我要给GA留出独立的类,而不是把变异和选择逻辑埋在某一个service里。这个类通常持有种群大小、迭代代数、变异率三个核心参数,并接受一个随机种子保证可复现。
public class GeneticScheduler implements SchedulingAlgorithm { private final int populationSize; private final int maxGenerations; private final double mutationRate; private final long seed; @Override public Schedule solve(SchedulingProblem problem) { Random random = new Random(seed); List<Chromosome> population = initPopulation(problem, populationSize, random); for (int gen = 0; gen < maxGenerations; gen++) { population = evolve(population, problem, random); double makespan = population.stream() .mapToDouble(ch -> fitness(ch, problem)) .min() .orElse(Double.MAX_VALUE); } Chromosome best = population.stream() .min(Comparator.comparingDouble(ch -> fitness(ch, problem))) .orElseThrow(); return decode(best, problem); } }这段代码里的Chromosome最简单就是一个List<Integer>,表示工序顺序编码;每次迭代都对种群进行锦标赛选择、顺序交叉和两点变异。适应度函数采用“最大完工时间Makespan + 拖期惩罚权重 * 总延迟时间”,权重系数需要从业务中取,常见做法是先在规则调度解上统计一次平均延迟,再按比例设定,避免一个目标压过另一个目标。
参数对算法的影响可以用下表解释:
| 参数 | 参考范围 | 对排产结果的影响 |
|---|---|---|
| populationSize | 100 ~ 500 | 过小早熟,容易收敛到局部最优;过大则迭代耗时上涨 |
| maxGenerations | 200 ~ 1000 | 控制搜索深度,超过1000代后收益显著下降 |
| mutationRate | 0.05 ~ 0.1 | 变异率过高会破坏优质工单序列,过低则难以跳出局部解 |
| crossoverRate | 0.8 ~ 0.9 | 保持父代有效基因块,一般不需要频繁调整 |
真正集成到框架里时,我会把算法参数和算法类分离,用配置文件或数据库字段保存,后续在界面上调参不用改源码。而在面试的“八股文”式追问里,遗传算法还会被问到如何保证收敛性,其实对车间调度场景来说,更看重的不是全局收敛证明,而是“相同输入能稳定重现在可接受时间范围内给出可行解”。因此随机种子seed会被显式暴露出来,线上出现问题时能按照seed复现执行现场,这是集成框架与教学demo差异最大的地方之一。
4. 基于Spring Boot的集成框架组装:异步调度接口、MES数据适配与参数配置
排产引擎设计得再干净,最终也要被MES/ERP系统调用。这里最常见的误区是拿同步HTTP请求去跑排产任务,一旦订单量上来,几十秒的求解过程会直接占满Tomcat线程。我会把排产框架拆成三个层:调度任务层负责异步提交与状态查询,数据适配层负责把外部工单转成领域模型,算法适配层根据工单规模自动路由到第3章的两个算法实现。下面直接给出可落地的源码骨架。
4.1 异步排产接口:为什么不能用同步请求承载长时间计算
多品种车间一次全量排产,耗时通常在几十秒到几分钟,因此接口要先提交任务、立即返回taskId,再由前端通过轮询或WebSocket获取结果。使用Spring Boot实现时,控制器的逻辑很薄,真正执行的任务丢给线程池。我一般会单独配置一个名称为scheduleTaskExecutor的ThreadPoolTaskExecutor,避免排产长任务阻塞主业务线程。
@Configuration public class ScheduleExecutorConfig { @Bean("scheduleTaskExecutor") public ThreadPoolTaskExecutor scheduleTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(2); executor.setMaxPoolSize(4); executor.setQueueCapacity(200); executor.setThreadNamePrefix("schedule-task-"); executor.initialize(); return executor; } }线程池参数设置的逻辑是:排产任务通常并发不高,但单个任务耗时很长,因此核心线程数不需要太大,给到CPU物理核数或核数减一即可;最大线程数设置4是为了应付临时同时提交两次大排产任务。queueCapacity也不是越大越好,过大会导致大批任务堆积在内存里,服务重启即丢失,所以在提交任务的同时应该往数据库写一条任务记录。
控制器层只需要暴露两个REST接口:一个提交排产,一个按taskId查状态。提交接口直接向线程池submit返回Future,再封装一个TaskInfo返回给调用方。
@RestController @RequestMapping("/api/schedule") public class ScheduleController { private final ScheduleTaskService taskService; @PostMapping("/run") public ResponseEntity<TaskInfo> submit(@RequestBody ScheduleRunRequest request) { String taskId = taskService.submit(request); return ResponseEntity.accepted().body(new TaskInfo(taskId, "PENDING")); } @GetMapping("/task/{taskId}") public ResponseEntity<TaskInfo> query(@PathVariable String taskId) { return ResponseEntity.ok(taskService.query(taskId)); } }这里的参数设计仍然遵循“排队优先于立即执行”的思想:接口为什么响应202而不是200?因为排产任务发起后,算法不一定在本机立即执行,可能被转发到独立的计算节点。把任务状态单独设计成一个对象,比直接返回Schedule对象更符合集成框架的扩展性要求,后续接RabbitMQ或Kafka时可以原样保留TaskInfo结构。
4.2 Repository模式接MES/ERP数据:把外部工单转成排产对象
排产框架不能直接复用MES系统的实体,否则一旦MES表结构调整,算法核心就会受牵连。常见做法是定义一层Repository/Adapter,在数据入口把MES/ERP数据转换成前面定义好的Job、Operation、Resource对象。这里以MyBatis为例,因为很多制造企业的Java技术栈都停留在MyBatis而不是JPA,而且如果看过MyBatis源码就明白,自带的BoundSql做的参数绑定比字符串拼接更安全,适配层只要专注字段映射。
@Repository public class MesOrderAdapter { private final MesOrderMapper mesOrderMapper; public List<Job> toJobs(String workshopId) { return mesOrderMapper.selectPendingOrders(workshopId).stream() .map(record -> new Job( record.getOrderNo(), toRelativeMinutes(record.getReleaseDate()), toRelativeMinutes(record.getDeliveryDate()) )) .toList(); } public List<Resource> toResources(String workshopId) { return mesOrderMapper.selectMachines(workshopId).stream() .map(m -> new Resource( m.getId(), m.getName(), toSkillSet(m.getCapabilities()), toCalendar(m.getWorkCalendar()) )) .toList(); } }在集成框架中使用这个适配层时,需要注意两个坑。第一,数据库时间必须统一换算成相对开工日的分钟数,否则不同服务器的时区和冬夏令时会让排产结果错乱;第二,MES返回的能力描述往往是逗号分隔的字符串,转换成Set 时要去除空格、统一大小写,否则skill.contains匹配不上。如果企业已经有消息中间件,也可以把MES的工单变更事件放进MQ,再由适配器监听事件增量更新内部缓存,避免每次排产前全量拉取。
| 接入方式 | 优点 | 适用前提 |
|---|---|---|
| 数据库直连查询 | 实现简单、重排快速 | MES开放只读库,数据实时性要求低 |
| REST接口拉取 | 业务隔离、权限可控 | MES第三方厂商提供稳定API |
| MQ事件订阅 | 增量更新、对源库压力小 | 已有Kafka/RocketMQ和约定消息协议 |
在源码工程里,我更倾向于先用数据库直连撑起第一个版本,同时把所有Repository方法定义在接口后面,后续迁移到MQ时替换实现类即可,不用改调用方的Service。
5. 排产框架源码验收技巧:固定随机种子、CI约束断言和人工锁定工序
排产框架上线前最怕两件事:一是有硬约束被算法绕过,二是相同的输入两次跑出完全不同的结果。第一类是正确性,需要把约束校验写进CI的自动化测试;第二类是稳定性,需要强制要求所有随机算法暴露种子参数。下面这两个技巧会直接影响源码有没有白做。
5.1 用两种手段把排产结果变成可验收制品
第一种手段是断言校验。把第2章的ScheduleValidator放进JUnit测试,针对生产的真实脱敏数据定时跑用例。只要后续有人改动领域模型中的predecessors或calendar,测试就能发现算法输出了非法时间。
@Test void scheduleShouldRespectResourceCapacity() { SchedulingProblem problem = loadDemoProblem(); Schedule schedule = new GeneticScheduler(200, 500, 0.08, 42L).solve(problem); assertTrue(new ScheduleValidator().validate(schedule, problem)); }测试代码里的42L就是固定随机种子,作用是把变异和交叉过程从随机变为确定性伪随机。真实排产框架会把seed和算法参数一起写入每次任务的日志上下文,这样不管是开发环境还是生产环境,只要taskId一致,复算得到的Assignment就完全一致。第二种手段是结果比对脚本,把旧的规则调度结果与遗传算法结果输出成JSON,比较总拖期时间、设备利用率和平均等待时间三个指标,形成一个自动化的可量化验收报告。
最后还要在源码工程里预留“人工锁定工序”的能力:设计一个AssignmentHolder,保存被计划员锁定的工序ID。排产算法碰到锁定工序时不参与变异和重分配,只是把剩余工序安排到没有冲突的空档。这件事看起来像是一个需求扩展,但如果在集成框架阶段没有预留hold字段,后面想加入人工拖拽甘特图,改动面会非常大,很可能从算法接口一直改到前端视图。取值上建议锁定粒度到工序,不要到整条工单,这样计划员的微调更多是排障补偿,而不是推翻整条产线。
本文还有配套的精品资源,点击获取