开篇:为什么是 APS 智能排产

一、场景:Excel 手排排到下午 2 点的车间主任
我做工业软件这些年,有两个项目让我印象最深。一个是 scada-collect 数据采集,去年已经交付过一家注塑厂,8000 个点位 1 秒刷新;另一个就是这个 APS 智能排产,同一个客户、同一批车间、同一个主任。
那天去客户现场做 PoC,正好赶上周一早班排产。主任姓王,四十来岁,车间干了 15 年,对每台注塑机的脾气了如指掌——哪台 120 吨的射速快、哪台 80 吨的模具经常卡、CNC 加工中心的冷却液每天下午 3 点要换一次。但他那天脸上没有平时的从容,桌上摊着三台笔记本,全是 Excel:
- 左边那本列工单:20 条,WO-20261013-001 到 WO-20261013-020,红色标记是今天要出货的,WO-007 优先级 1、WO-003 客户投诉过两次、后面排的 WO-015 是下午新插进来的加急单。
- 中间那本列设备:12 台注塑机 + 4 台 CNC + 2 套模具。注塑机分两班倒(白班 08:00-20:00,夜班 20:00-08:00),CNC 是三班制,模具数量有限——MOLD-001 只有 2 套,MOLD-002 只有 1 套,周一早上还在维修。
- 右边那本列排产计划:灰色的格子是已经开工排不动的(周五晚上锁的计划),白框是今天要排的,用箭头连接着各个工序的前后关系。
"排完这些,我基本下午 2 点了。"王主任叹了口气,"然后 ERP 那边下午 3 点又会来一批新工单,还得重新调一遍。"
我看了一眼他的 Excel——12 台设备 × 20 个工单 × 平均 3 道工序,就是 720 个格子。每个格子要考虑:这台机器此刻空闲吗?换型要 15 分钟还是 45 分钟?模具够不够?下一道工序的 CNC 能不能跟上?白班剩 4 小时,这个 60 分钟的工序能不能插进去?
这不是填表,这是在解一个约束满足问题(Constraint Satisfaction Problem)。而且是动态的——插单、撤单、设备故障、模具维修,每天都在变。
二、SCADA 是管道,APS 是求解器
写 SCADA 连载的时候,我把数据采集比作"管道"——PLC4X Driver 连设备,采集任务调度跑循环,TDengine 落盘。整条链路是确定性的:来一个值,收一个值,存一个值。管道不通就查 Driver,丢数据就查 ReplayBuffer,逻辑是线性的。
APS 完全不同。它是求解器——给定一堆工单、一堆设备、一堆约束,你要在几亿种排列组合里找一个"最优解"。这不是管道能解决的问题,这是 NP-hard。
两者的本质差异:
| 维度 | SCADA 管道 | APS 求解器 |
|---|---|---|
| 输入 | PLC 寄存器值(确定性流) | 工单列表 + 设备列表 + 约束(组合空间) |
| 输出 | 时序数据 | 排产计划表(设备 × 时间的二维分配) |
| 核心问题 | "这个值能不能收上来" | "这道工序该排在哪台设备的哪个时间段" |
| 失败原因 | 网络断了、协议错了 | 约束冲突(硬约束违反)、解不够好(软约束) |
| 技术栈 | Driver + ScheduledExecutor + TDengine | Timefold Solver + ConstraintProvider + Planning 层 |
Timefold 约束求解第一次跑通那天,我记得很清楚。王主任在我电脑边盯着屏幕看了 30 秒——甘特图刷新出来,100 个工序整整齐齐地排在 16 台设备上,硬约束全满足(机器不重叠、日历不跳过、模具不超量),Makespan 从他手动排的 18 小时压到 14.2 小时。他瞪大眼问:"这也能行?"
我没告诉他,这 30 秒里求解器在大约 2.4 亿种组合里帮他找了这一个。
三、行业对比:商用 vs 自研 vs 启发式 vs Timefold
在决定技术路线之前,我把市面上四类方案都摸了一遍——和 SCADA 连载里对比 ThingsBoard / FactoryTalk / Ignition 是同一种尽职调查。
第一类:商用 APS 套件。Oracle Demantra、SAP IBP、西门子 Plant Simulation,这些是 ERP 巨头绑定的 APS 模块。优势是和 ERP 深度集成(工单从 ERP 下来直接进 APS,排产结果直接写 ERP),约束引擎是商用级的(支持多工厂协同、物料齐套检查、产能负荷分析)。缺点也极其致命——授权费按用户数或工单数收费,一套下来几十万甚至上百万,部署周期至少 3 个月起,而且黑盒。有个朋友在某厂实施 Demantra,调约束调了两周才让求解器识别出"我们厂晚班是 20:00-08:00 跨天班次"这个细节——Oracle 官方的约束模板里只支持 start<end 的白班。中小制造厂绝对用不起。
第二类:自研启发式规则引擎。这是很多小厂 IT 部门的选择——写一堆规则:"优先排高优先级工单"、"优先分配最早空闲的机器"、"换型时间短的先排"。实现快,1-2 周就能跑起来。但天花板太低——遇着模具冲突 + 换型冲突 + 换班冲突同时卡壳的场景,规则引擎兜不住。比如 MOLD-001 只有 2 套,3 个高优先级工单都想用它,规则引擎会"先到先得"把前两个排上,第三个就搁那儿了;Timefold 会在几秒内算出:把其中一个工单换到 MOLD-002,虽然换型时间多了 15 分钟但模具够,而且整体 Makespan 更短。规则引擎是贪心,Timefold 是全局搜索。
第三类:商用排产引擎。比如 Preactor(现在被 Siemens 收了)、Asprova。这些是专用排产引擎,约束建模比 ERP 内置的灵活,API 是开放的,可以嵌入到自己的系统里。但授权费依然贵——Asprova 单机授权 10w+,而且学习曲线陡峭。我们需要的约束只有 7 条(3 硬 4 软),用不到 Asprova 那套复杂的物料齐套 + 多工厂协同 + 车间实时反馈。
第四类:开源约束求解框架。这就是 Timefold(前身是 OptaPlanner)。它不是一个"完整的 APS 系统",而是一个求解器内核——你给它问题建模(PlanningSolution / PlanningEntity / PlanningVariable)、给它写约束(ConstraintProvider)、给它一个终止条件(30s 超时),它帮你找最优解。Timefold 的优势:Java 原生,和 Spring Boot 无缝集成,原 OptaPlanner 团队维护,社区活跃,Apache 2.0 许可商用友好。对我们来说,它正好卡在"不想自造求解器但又不想被商用 APS 锁死"这个夹层里。
横向比完,结论和 SCADA 选 PLC4X 完全一样:不自造轮子,但自建适配层。 PLC4X 是工业协议库,我们写 Driver 适配层屏蔽协议差异;Timefold 是约束求解框架,我们写 Planning 层 POJO 屏蔽框架注解细节。
四、选型:为什么选 Timefold Solver
最终拍板引入 Timefold 0.8.x。几个决策细节值得讲清楚:
版本选择:0.8.x 而非 2.x。pom.xml 里写的是 ai.timefold.solver:timefold-solver-core:0.8.42,不是最新的 2.x。原因是 yudao 父工程是 jdk8-snapshot,Timefold 2.x 强制要求 Java 17+。0.8.x 是 OptaPlanner 改名后的第一个系列,兼容 Java 8+,功能上已经完整覆盖了 Job Shop Scheduling 需要的所有能力(Construction Heuristic、Local Search、Constraint Stream、HardSoftScore)。升到 2.x 是未来的事——适配层把版本差异封在 ApsScheduleSolution 和 ApsPlanningTask 里,升级只改注解不动业务代码。
为什么不直接用 OptaPlanner。OptaPlanner 是 Timefold 的前身,Red Hat 维护,Java 8 兼容做得更好。但 OptaPlanner 已经停止活跃开发了(最后一个 release 是 8.44.x,2023 年底),Timefold 是同一团队的新项目,持续更新。选 Timefold 而不是 OptaPlanner 是为了长期维护性——2.4.0-jdk8-snapshot 虽然老,但我们不想绑在一个停更的依赖上。
核心取舍:约束写死 7 条,不做可配置化。Timefold 官方示例里约束是通过 ConstraintFactory.forEach().join().filter().penalize() 链式调用写的,有能力做成前端可配置化(类似规则引擎)。但我没这么做——7 条约束是注塑厂排产的完整集合(机器不重叠、日历尊重、模具配额、Makespan 最小化、换型时间最小化、延迟最小化、优先级加权),前端配置化会让 ConstraintProvider 变复杂一个数量级,而且中小工厂的计划员根本没能力自己写 Timefold 约束。写死 7 条 + 给每条加权重配置,是"够用且好维护"的平衡点。
适配层设计:5 个核心类,隔离框架。这是整个 APS 项目最关键的架构决策。5 个类把 Timefold 的所有注解(@PlanningSolution / @PlanningEntity / @PlanningVariable / @ValueRangeProvider / @ScoreProperty / @ProblemFactCollectionProperty)封在内部,业务代码(Service 层、Controller 层、DO 层)完全不依赖 Timefold API。
// cn.iocoder.yudao.module.aps.framework.aps.solver.ApsScheduleSolution
@TimefoldAnnotation.PlanningEntityCollectionProperty
private List<ApsPlanningTask> taskList;
@TimefoldAnnotation.ProblemFactCollectionProperty
private List<PlanningMachine> machineList;
@TimefoldAnnotation.ScoreProperty
private HardSoftScore score;
@TimefoldAnnotation 是一个占位注解类(cn.iocoder.yudao.module.aps.framework.aps.solver.TimefoldAnnotation),真实 Timefold 依赖在 classpath 上时,把这个类里的注解换成 ai.timefold.solver.core.api.domain.solution.PlanningSolution 等原生注解就行。Service 层和 DO 层零修改。
五、代码骨架:73 个 Java 文件 / 6 张表 / 7 条约束
整个 APS 模块的规模比 SCADA 略大(SCADA 是 5 张表 + 4 个 Driver + 9 个 Java 文件),APS 是 73 个 Java 文件 + 6 张 PostgreSQL 表 + 7 条约束。和 SCADA 保持同一套芋道脚手架的架构模式:
yudao-module-aps/
├── framework/aps/
│ ├── solver/
│ │ ├── ApsScheduleConstraintProvider.java ← 7 条约束(3 硬 4 软)
│ │ ├── ApsScheduleSolution.java ← PlanningSolution
│ │ ├── ApsPlanningTask.java ← PlanningEntity
│ │ ├── PlanningMachine.java ← ProblemFact
│ │ ├── PlanningMold.java ← ProblemFact
│ │ ├── HardSoftScore.java ← Score 实现
│ │ └── TimefoldAnnotation.java ← 占位注解(替换 Timefold 原生)
│ ├── calendar/CalendarUtil.java ← 班次日历计算(跨天/午休/节假日)
│ ├── conflict/ConflictDetector.java ← O(1) 区间冲突检测
│ └── gantt/GanttAssembler.java ← 甘特图 VO 组装
├── service/solver/ScheduleSolverServiceImpl.java ← 排产主入口(DO → Planning → 求解 → 回填)
├── dal/dataobject/ ← 6 个 DO(WorkOrder / Task / Resource / Calendar / Mold / SchedulePlan)
├── controller/admin/ ← 7 个 Controller(工单/工序/资源/日历/模具/排产计划/甘特图)
└── enums/ ← 6 个枚举(SolverType / ResourceType / MoldStatus 等)
73 个 Java 文件里,核心逻辑集中在 framework/aps/(约 15 个)和 service/solver/(2 个)里,Controller/VO/Mapper/DO 都是 MyBatis-Plus 和 yudao 代码生成器自动生成的样板代码。真正有价值的就是那 17 个文件——理解了它们,就理解了整个 APS 系统。
六、商用化检查清单(开篇版)
和 SCADA 连载一样,每章末尾放一个商用化检查清单,方便上线前过一遍:
- Timefold 求解耗时可接受吗?100 工序 × 20 机器 × 30s 超时,本地压测 HardSoftScore.isFeasible() 返回 true 吗?
- 适配层注解替换验证过吗?pom.xml 加 Timefold 依赖后,TimefoldAnnotation 能正确代理到原生注解吗?
- 日历跳过正确吗?排产结果里所有 Task 的 planned_start_time 和 planned_end_time 都在工作时段内?
- 模具不超量验证过吗?同时间窗口内同模具的占用数量 ≤ quantity?
- 多人并发排产怎么处理?乐观锁 version 字段 + OptimisticLockException 全局异常处理,冲突时前端弹框提示刷新?
- 插单重排的 keepLocked 验证过吗?locked=true 的 Task 在 reschedule 后保持不变?
七、下一章预告:6 张表怎么建模
到这里,整个 APS 项目的定位和选型讲清楚了——求解器 vs 管道、Timefold vs 自研、适配层 vs 直接用。但所有求解的前提是领域模型建对了——工单怎么描述优先级?工序为什么用 JSONB 存候选设备?产能系数怎么定义?班次日历怎么处理跨天和午休?
下一章我会把这 6 张表从头到尾讲一遍,每张表的设计决策、踩过的坑、为什么这样设计而不是那样设计——和 SCADA 连载里"5 张表说清整个系统"是同一种结构,但 APS 多了日历和模具两张表,因为排产"跟着日历走",而不是 SCADA 那样 24 小时无间断。
