做调度管控类系统,最怕的就是业务建模跑偏。前阵子刚把一套基于 SpringBoot 的预制菜调度管控系统从零搭完,从需求分析到数据库设计再到最后部署上线,踩的坑比预想的多得多。这个项目不是简单的 CRUD 堆功能,它的核心难点在于"调度"两个字——订单来了怎么排产、库存怎么锁、中央厨房产能怎么分配,每一环都要有清晰的业务闭环。这篇就把整个设计和实现过程拆开讲,包括模块划分、表结构设计、调度核心逻辑的代码思路,以及部署文档里容易忽略的细节,给正在做同类系统或者准备拿这个方向做毕业设计的同学一个完整参考。
1. 预制菜调度为什么难做:先搞清楚业务边界,再谈技术实现
预制菜调度管控系统,本质上是一个打通"订单 → 生产排程 → 仓储配送"的数据中枢。但预制菜行业和普通电商、普通制造业都不一样,它有自己非常特殊的调度约束。
1.1 预制菜调度和普通订单系统的本质差异
我去看了很多类似的毕设项目或者开源系统,发现大部分都把预制菜调度做成了普通的进销存+订单系统。这是最大的误区。普通订单系统只管"卖什么、卖多少、发给谁",但预制菜调度管控还必须回答三个额外问题:
- 这批订单什么时候必须下线? 预制菜有保质期,通常是冷藏3-7天,冷冻30-90天。生产计划必须倒排,不能无限累积库存。
- 中央厨房的产能够不够? 每条产线每天能处理多少份菜品是固定的,高峰期的订单要排到哪条产线、哪个时段,需要全局统筹。
- 原材料和净菜库存够不够支撑这批生产? 调度排产之后要立刻检查物料齐套率,缺料要能提前预警,而不是等生产线上停了工才发现。
所以在做系统设计的时候,我给自己定了一条原则:调度管控的核心是"生产任务"这一层,订单只是触发源。系统真正要管理好的对象是生产计划、产线资源、物料需求和库存水位,而不是仅仅管理订单本身。
1.2 系统的目标用户和使用场景
这套系统面向的角色有四个:运营人员、计划调度员、仓库管理员、产线班组长。不同角色关心的数据维度完全不同。
| 角色 | 核心诉求 | 对应功能 |
|---|---|---|
| 运营人员 | 能接单、能看订单进度 | 订单管理、订单状态跟踪 |
| 计划调度员 | 排产、分配产线、处理插单 | 生产计划排程、产能看板 |
| 仓库管理员 | 知道备什么料、什么时候备 | 物料需求计算、领料出库 |
| 产线班组长 | 知道今天做什么、做多少 | 生产任务下发、完工上报 |
这个定位决定了系统不能做成一个大而全的 ERP,而是要把有限的精力聚焦在"调度"这条主线上。我在做需求梳理的时候,直接砍掉了很多看似有用但实际跑不通的功能,比如供应商对账、客户关系管理、财务结算,这些在毕设或者中小型企业的初步信息化建设中都不需要,加上去反而会让整个系统变得臃肿,调度这个核心模块的深度就不够。
1.3 调度系统的核心业务流程闭环
整个系统的业务主链路是这样的:
- 运营在系统里创建销售订单,订单中包含多个菜品明细。
- 系统根据订单菜品自动计算生产需求总量,并合并同类菜品的需求。
- 计划调度员查看产能日历,选定排产日期和产线,系统自动生成生产工单。
- 生产工单下发后,系统根据菜品配方(BOM)自动展开生成物料需求清单。
- 仓库根据物料需求清单进行领料备料,生产完工后入库。
- 成品库存满足订单后,系统自动关联订单状态,进入发货环节。
这条链路里最关键的是第 2 步到第 4 步的联动逻辑,也是后面代码实现里最复杂的部分。很多项目做到第 1 步和第 5 步就停了,实际上调度系统的价值恰恰隐藏在这些中间环节。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从需求到模块拆分:六个核心模块的职责边界
模块拆分决定了整个项目的开发难度和维护成本。我采用的方式不是传统的按"增删改查"拆,而是按业务域来拆,每个模块只负责自己领域中完整闭环的业务逻辑。
2.1 模块总览与依赖关系
整个系统拆成六个核心模块:
- 系统管理模块:用户、角色、权限、菜单管理。这个模块和调度业务没有直接关系,但是是系统的基石,用的是标准的 RBAC 模型,SpringSecurity+JWT 做认证授权。
- 基础资料模块:菜品信息、配方 BOM、产线信息、物料档案、客户档案。这些是调度系统的基础数据,相当于整个系统的"字典"。
- 订单管理模块:销售订单的创建、审核、查询、状态流转。订单是调度系统的触发入口。
- 计划排产模块:这是整个系统的核心。负责生成生产计划、合并需求、检查产能、创建生产工单、处理插单和异常。
- 仓储管理模块:库存台账、物料入库、领料出库、成品入库、库存预警。
- 生产执行模块:工单下发、产线任务接收、完工上报、工时记录。
模块之间的关系是一条清晰的单向依赖链:基础资料 ← 订单 ← 排产 ← 仓储/生产执行。基础资料模块不依赖任何业务模块,排产模块依赖订单和基础资料,仓储和生产执行依赖排产生成的工单。这样设计的好处是,任何一个模块内部做调整,上下游只需要保持接口稳定就不会出大问题。
2.2 为什么把"计划排产"单独拆成一个模块而不是放在订单里
这是我在设计过程中比较坚持的一个点。很多同类项目把排产功能直接塞进订单管理里,导致订单状态和生产状态强耦合。一旦出现插单、延期、拆分生产等异常情况,订单里的字段根本不够用,代码会越写越乱。
我把生产计划拆出来之后,订单和工单之间是"多对多"的关系:一个订单可以对应多个生产工单(因为产能不足可能要分两批生产),一个生产工单也可以合并多个订单的同类菜品需求。订单只管自己有没有被满足,满足到什么程度由"订单-工单关联表"来维护。这样订单模块就保持很纯粹的状态机流转,排产模块负责处理所有的调度逻辑,互不干扰。
2.3 每个模块内聚的功能清单
举两个例子说明模块内部的功能组织方式。
排产模块内部拆成"需求汇总、产能校验、工单生成、排产调整"四个子功能。需求汇总是把订单中同一个菜品在同一个交货周期内的所有数量加起来,形成一个"待排产需求池";产能校验是判断这个菜品关联的产线在指定日期内的剩余可用产能是否足够;工单生成满足产能校验后落地生产工单;排产调整则是处理人工介入的情况,比如紧急订单强制插入。
仓储模块内部则拆成"库存流水、物料批次、预警规则"三个子功能。所有库存变动必须走库存流水记录,不允许直接改库存表的数值,这个约束在后面会详细说明。
3. 技术选型与架构设计:SpringBoot 够用且不浮夸
说句实在话,这类调度管控系统在数据量级和并发量上,远没到必须上微服务或者分布式架构的程度。选 SpringBoot 单体应用是足够支撑的,而且开发效率最高。
3.1 技术栈的具体选择和理由
后端:SpringBoot 2.7.x + MyBatis-Plus + MySQL 8.0。
没有选择 JPA 是因为调度系统里有大量复杂的多表联查、动态查询条件和分组聚合操作,MyBatis-Plus 写 SQL 更直观可控。SpringBoot 2.7 版本现在非常成熟,生态里集成的组件对 2.7 的支持也最好,没必要强行追 SpringBoot 3.x。
权限认证:SpringSecurity + JWT。
系统有四个角色,权限模型相对简单,用 SpringSecurity 做资源访问控制,JWT 做无状态认证。JWT 的好处是后端不用维护 session,前端拿到 token 放请求头就行,部署到服务器上也不需要配置 session 共享。
前端:Vue 2 + Element UI。
Vue 2 生态成熟稳定是最大优势,Element UI 的表格、表单、弹窗组件对这种管理后台类型的业务系统覆盖得非常全面。前端不做 SSR,不做复杂的权限路由,就是一个标准的中后台管理界面。
中间件:Redis 做数据缓存,不依赖消息队列。
生产计划排程结果、菜品基础信息这些不经常变的数据缓存到 Redis,可以减少数据库压力。一些项目喜欢引入 RocketMQ 或者 RabbitMQ 做订单状态变更的通知,我在这个项目里没有用,因为业务量没到那个级别,直接把订单状态变更和后续逻辑串行执行就可以,减少部署复杂度。
3.2 前后端分离的接口设计约定
整个系统的接口设计遵循 RESTful 风格,但做了几个统一约定:
- 所有接口路径以
/api开头,后面跟模块名和资源名,比如/api/plan/work-order。 - 统一返回结构
Result<T>,包含 code、message、data 三个字段。code=200 表示成功,非 200 表示业务失败。 - 列表查询统一封装 PageResult 结构,前端只需要传 pageNum 和 pageSize,后端返回 total 和 records。
- 所有写操作接口必须传操作人 ID,由后端统一填充创建人、创建时间、修改人、修改时间字段。
这些约定虽然简单,但能大幅减少前后端联调时的沟通成本。前端只认这一套规范,后端也不用为每个接口单独设计返回结构。
3.3 架构分层和包结构
代码分层采用的是标准的 Controller → Service → Mapper 三层架构,但我在 Service 层做了一点调整:把"事务控制"和"业务编排"分开。
比如排产这个动作,涉及修改订单状态、创建工单、生成物料需求、更新产线产能占用,这四步必须在一个事务里完成。我就单独写了一个 PlanService#createWorkOrder() 方法,方法上标注 @Transactional(rollbackFor = Exception.class),在这个方法内部依次调用四个私有方法,保证原子性。如果未来这个流程中间要插入消息通知之类的逻辑,可以在方法内部增加,不影响事务边界。
包结构按照模块分包而不是按技术层分包:
code复制com.example.pre-dish
├── common // 通用工具、统一返回、异常处理
├── config // 配置类(Redis、Security、MyBatisPlus)
├── security // 认证授权相关
├── module
│ ├── system // 系统管理模块
│ ├── base // 基础资料模块
│ ├── order // 订单管理模块
│ ├── plan // 计划排产模块
│ ├── warehouse // 仓储管理模块
│ └── production // 生产执行模块
└── framework // 框架封装、拦截器、切面
这种按模块分包的方式在项目初期可能看不出太大优势,但当代码量超过两万行之后,找代码的效率比按技术层分包高得多。你想找排产相关逻辑,直接进 plan 包,不用在 controller/service/mapper 三个包之间来回跳。
4. 数据库设计的关键决策:用状态机约束业务,用流水表约束库存
数据库是整个调度系统最容易被低估的部分。我第一次设计表结构的时候也犯过不少错误,后来通过重构才稳定下来。几个关键的设计决策值得展开讲讲。
4.1 订单和生产工单的状态机设计
订单状态我设计成 待审核 → 已审核 → 排产中 → 已完工 → 已发货 → 已完成,外加两个终止状态 已取消 和 已驳回。生产工单状态是 待下发 → 已下发 → 生产中 → 已完工 → 已入库。
每一个状态的流转必须在代码里显式校验,不能靠前端传什么就改什么状态。我在订单实体里加了一个 status 字段,在 Service 层定义了一个枚举 OrderStatusEnum,状态变更统一走 transition(currentStatus, targetStatus) 方法,里面是一个 switch-case 判断合法流转路径。非法状态流转直接抛业务异常。
这样设计的好处是,调度排产的时候只需要根据订单状态判断能不能排:只有"已审核"状态的订单才能进入排产流程,防止运营还没审核就排产。工单同理,只有"已下发"状态的工单才能开工上报。
4.2 物料需求展开和库存扣减:账实分离思路
调度系统里最容易出 bug 的就是库存管理。我采用的模式是账实分离:库存表只存当前可用库存,每次变动都追加一条库存流水记录。
物料出入库的流程是:仓库根据物料需求单领料出库,出库时插入一条出库流水,同时扣减库存表当前值。这样的话,任何时候想看"某个物料某段时间内出了多少、入了多少",直接查流水表聚合就行,库存表的数值只是个冗余缓存,以流水汇总结果为准。
这个设计在面对"领料后退回""生产报废补料"这些特殊场景时特别有用。如果只有一张库存表,每处理一次异常都要去翻原始单据,操作记录很容易乱。有了流水表,所有异常都可以通过新增一条冲正流水来处理,账永远是对得上的。
4.3 核心表结构清单
整个数据库一共是 24 张表,我列一下和调度主链路强相关的关键表:
| 表名 | 作用 | 关键字段 |
|---|---|---|
sys_user |
用户表 | id, username, password, dept_id |
pdc_dish |
菜品表 | id, dish_name, shelf_life_days, spec |
pdc_bom |
配方表 | id, dish_id, material_id, qty |
pdc_material |
物料表 | id, material_name, unit, safety_stock |
pdc_production_line |
产线表 | id, line_name, daily_capacity, status |
sale_order |
销售订单主表 | id, order_no, customer_id, status |
sale_order_item |
订单明细表 | id, order_id, dish_id, qty, delivery_date |
plan_demand_pool |
需求池表 | id, dish_id, total_qty, plan_date, status |
plan_work_order |
生产工单表 | id, work_order_no, line_id, dish_id, qty, plan_date, status |
plan_order_work_order_rel |
订单工单关联表 | id, order_id, work_order_id, qty |
wh_stock |
库存表 | id, material_id, stock_qty, warehouse_id |
wh_stock_flow |
库存流水表 | id, material_id, change_type, change_qty, before_qty, after_qty, biz_no |
wh_requirement |
物料需求表 | id, work_order_id, material_id, require_qty, status |
注意几个容易忽略的字段:plan_demand_pool 里的 plan_date 是计划生产日期,用来做日期维度的产能汇总;sale_order_item 里的 delivery_date 是客户交货日期,排产要根据交货日期倒推最晚生产日期;wh_stock_flow 里的 before_qty 和 after_qty 是用来做库存变动审计的,任何一个库存异常都能追溯到具体操作之前的量。
4.4 库存预警规则的设定
为了避免备料不足影响生产计划,我设置了两个维度的预警:
- 安全库存预警:当物料当前库存低于安全库存时,在仓储看板里置红提示。
- 需求覆盖预警:当物料的需求总量(由未完工工单展开得到)超过当前库存 + 预期到货量时,提前提示调度员需要采购。
第二个预警是排产模块触发的,生成工单的时候系统会自动跑一遍物料齐套检查,如果齐套率不足 80%,工单状态依然是"待下发",但是会打上"物料不足"的标记,只有调度员手动确认才能继续下发。这个流程我踩过坑,最开始是直接阻塞下发,后来发现实际业务中库存不足的时候调度员可能要调整其他订单的优先级来腾库存,不能一刀切。
5. 调度核心逻辑的实现:需求合并、产能校验、工单生成
到达整个系统最核心的部分,也就是"调度"这个动作是怎么用代码实现的。我用一个完整的排产流程来展开。
5.1 需求合并:从订单明细到需求池
订单审核通过后,系统会执行一次需求汇总。需求汇总的规则是:同一个菜品、同一个计划生产日期、所有未取消的订单数量相加。
java复制public void aggregateDemand() {
// 查出所有已审核且未排产的订单明细
List<OrderItem> items = orderItemMapper.selectList(
new LambdaQueryWrapper<OrderItem>()
.eq(OrderItem::getStatus, "UNPLANNED")
.in(OrderItem::getOrderId, validOrderIds)
);
// 按菜品ID分组汇总数量
Map<Long, Integer> demandMap = items.stream()
.collect(Collectors.groupingBy(
OrderItem::getDishId,
Collectors.summingInt(OrderItem::getQty)
));
// 写入需求池表
demandMap.forEach((dishId, totalQty) -> {
DemandPool pool = new DemandPool();
pool.setDishId(dishId);
pool.setTotalQty(totalQty);
pool.setPlanDate(nextProductionDate());
pool.setStatus("WAITING");
demandPoolMapper.insert(pool);
});
// 更新订单明细状态为已归集
items.forEach(item -> {
item.setStatus("PLANNED");
orderItemMapper.updateById(item);
});
}
这段代码有几个细节:订单明细的 status 字段用来标记这条明细是否已经进入需求池,防止重复汇总;nextProductionDate() 是根据交货日期倒推的,规则是交货日期减去保质期天数,再留出半天的安全缓冲;聚合操作放在事务里执行,避免需求池写了一半订单状态已经改了的中间状态。
5.2 产能校验:生产计划排程的判断逻辑
需求池生成之后,调度员在前端选择要排产的日期和产线。系统要做的是校验该产线在计划日期当天的剩余产能是否足够。
产能校验的核心 SQL 是查询该产线当天已排产工单的总量:
sql复制SELECT COALESCE(SUM(plan_qty), 0)
FROM plan_work_order
WHERE line_id = #{lineId}
AND plan_date = #{planDate}
AND status IN ('ISSUED', 'IN_PROGRESS')
用产线日产能减去已排产量,得到剩余可用产能,再和需求池里的待排数量对比。如果够,则生成工单;如果不够,系统会提示调度员选择拆分排产或者调整到其他产线。
拆分排产的逻辑是:如果订单数量 2000 份、产线日产能 1500 份,系统自动拆成两个工单,一个 1500 份排到当天,另一个 500 份排到下一天。这个拆分逻辑在代码里就是个简单的数量比较和分配:
java复制public List<WorkOrder> splitByCapacity(DemandPool pool, int lineCapacity) {
List<WorkOrder> result = new ArrayList<>();
int remaining = pool.getTotalQty();
Date currentDate = pool.getPlanDate();
while (remaining > 0) {
int assignQty = Math.min(remaining, lineCapacity);
WorkOrder order = new WorkOrder();
order.setDishId(pool.getDishId());
order.setQty(assignQty);
order.setPlanDate(currentDate);
order.setLineId(selectedLineId);
result.add(order);
remaining -= assignQty;
currentDate = DateUtil.offsetDay(currentDate, 1);
}
return result;
}
这里要注意,拆分的时候不能把同一个订单的同一个菜品拆得太碎。比如一个订单要 100 份,拆成 50 和 50 两批是合理的,但拆成 1 份一批就不合理了。所以我在拆分逻辑里加了一个最小批次限制,低于这个限制的订单不做拆分,而是整体顺延到下一天。
5.3 工单生成与物料需求自动展开
工单生成后,紧接着要做的是根据配方 BOM 自动展开物料需求。这一步也是调度系统的一个亮点功能,它把"排产"和"备料"串联起来了。
java复制@Transactional(rollbackFor = Exception.class)
public void createWorkOrderWithMaterialRequirement(List<WorkOrder> workOrders) {
for (WorkOrder workOrder : workOrders) {
workOrderMapper.insert(workOrder);
// 根据BOM展开物料需求
List<BomItem> bomItems = bomMapper.selectList(
new LambdaQueryWrapper<BomItem>()
.eq(BomItem::getDishId, workOrder.getDishId())
);
for (BomItem bomItem : bomItems) {
Requirement requirement = new Requirement();
requirement.setWorkOrderId(workOrder.getId());
requirement.setMaterialId(bomItem.getMaterialId());
requirement.setRequireQty(
bomItem.getQty().multiply(BigDecimal.valueOf(workOrder.getQty()))
);
requirement.setStatus("PENDING");
requirementMapper.insert(requirement);
}
// 更新需求池状态
demandPoolMapper.updateStatusToPlanned(workOrder.getDemandPoolId());
}
}
这里有一个非常容易出现的问题:BOM 表里的用量单位可能是"克"或者"份",而工单数量单位是"份",如果在数据库设计的时候没有约定好计量单位,乘出来就全是错的。我在基础资料模块中给每个物料增加了 unit 字段和 conversion_rate 转换率字段,所有 BOM 展开计算的 SQL 和代码里都带单位换算,最大程度避免了这种低级错误。
物料需求生成之后,仓库模块会有一个待处理任务列表,仓管员根据需求单逐项备料。备料完成之后,需求单状态变为"已备料",工单状态变为"可开工"。
5.4 插单和异常处理的调度策略
实际运营中插单是常态。比如一个客户临时加了一个大订单,要求后天交付。系统里我实现了一个简易的插单策略:
- 新订单进入需求池,标记为
URGENT。 - 排产页面向调度员展示当前的产能占用情况,按产线和日期维度。
- 调度员选定要挤占的产线和日期,系统会找出该产线当天已排但未开工的工单。
- 调度员选择"顺延"或"合并"两种方式处理被挤占的工单。
- 系统自动调整被挤占工单的计划日期,并重算物料需求日期。
这个逻辑在代码里就是一组"工单日期调整"的服务方法,核心是保证被调整的工单对应的物料需求日期也跟着顺延,避免仓库提前备料造成浪费。插单功能是我花时间最多的一个点,因为边界情况特别多,比如被挤占的工单如果已经开始生产了就不能顺延,只能建议调度员和客户协商。
6. 部署运维的完整落地:从本地开发到服务器上线
代码写完之后,部署环节也是很多项目的重灾区。这个系统的部署文档我整理了三套环境:本地开发环境、演示环境、生产环境,部署方式从简到繁,但核心思路是一致的——用 Docker 做容器化部署,降低环境差异带来的问题。
6.1 环境准备的完整清单
部署之前要准备这些基础环境:
- 一台 Linux 服务器:我用的是 CentOS 7.9,2核4G 的配置足够跑整套系统。
- Docker 和 Docker Compose:用来装 MySQL、Redis 和打包后的应用容器。
- JDK 1.8 环境:本地开发用的 JDK 版本是 1.8,打包后生成 jar 包。
- Maven 3.6+:用于项目依赖下载和打包。
- Node.js 14+:前端项目构建环境。
部署文档里最重要的一步是环境变量的配置。SpringBoot 项目的 application.yml 里我用了 ${MYSQL_HOST}、${REDIS_HOST} 这种占位符的方式,在实际部署时通过 Docker Compose 的环境变量传入,避免把数据库密码硬编码在配置文件里。
6.2 Docker 部署的具体步骤
后端服务我用一个 Dockerfile 打镜像,核心内容:
dockerfile复制FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/pre-dish-system.jar
COPY ${JAR_FILE} app.jar
ENV JAVA_OPTS=""
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
前端我用 Nginx 容器来托管构建后的静态资源,同时配置反向代理把 /api 请求转发到后端容器。
整套部署用 Docker Compose 统一编排:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root123456
MYSQL_DATABASE: pre_dish_db
ports:
- "3306:3306"
volumes:
- ./mysql-data:/var/lib/mysql
redis:
image: redis:6-alpine
ports:
- "6379:6379"
backend:
build: ./backend
depends_on:
- mysql
- redis
environment:
MYSQL_HOST: mysql
MYSQL_PORT: 3306
MYSQL_DB: pre_dish_db
MYSQL_USER: root
MYSQL_PASSWORD: root123456
REDIS_HOST: redis
REDIS_PORT: 6379
ports:
- "8080:8080"
frontend:
build: ./frontend
ports:
- "80:80"
部署时只需要在服务器上执行 docker-compose up -d,整套系统就起来了。初始化数据库的时候,我提供了一个 init.sql 脚本,里面包含建表语句和基础数据。注意第一次启动 MySQL 容器时,数据库是自动创建的,但需要等待 MySQL 初始化完成,所以后端容器启动时可能会报连不上数据库。我在部署文档里专门加了一段:先启动 MySQL 和 Redis,等一分钟确认数据库就绪后再启动后端容器。这个顺序问题如果没写进文档,第一次部署大概率会翻车。
6.3 部署过程中最容易踩的坑
部署过程中我实际遇到的几个坑,都是在文档里反复强调过的:
- MySQL 8.0 的认证插件问题:MySQL 8.0 默认的认证方式是
caching_sha2_password,但 JDBC 驱动版本低了就不认。解决办法是在 Docker Compose 的环境变量里加MYSQL_ROOT_PASSWORD之外,还要在连接串中指定useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai。 - 打包时跳过测试:
mvn package -DskipTests这个命令一定要加,否则单元测试中的数据库连接会因为连不上远程数据库而失败,导致打包中断。 - 服务器端口开放:云服务器默认安全组只开放 22 端口,把 80 和 8080 端口加进安全组规则才能访问,这个坑虽然低级,但是很多人第一次部署都会卡在这一步。
- 前端跨域问题:生产环境前端和后端在同一个域名下,Nginx 做了反向代理所以不存在跨域;但本地开发环境前后端分离,需要后端配置
CorsFilter允许指定来源跨域。部署文档里要区分说明,否则本地联调接口会一直报跨域错误。
6.4 数据库备份和恢复的实用脚本
调度系统上线后,数据就是企业的核心资产。我写了一个简单的备份脚本,每天凌晨 2 点用 crontab 执行:
bash复制#!/bin/bash
BACKUP_DIR=/data/backup/mysql
DATE=$(date +%Y%m%d%H%M%S)
docker exec pre-dish-mysql mysqldump -uroot -p$MYSQL_ROOT_PASSWORD pre_dish_db > $BACKUP_DIR/pre_dish_$DATE.sql
find $BACKUP_DIR -type f -mtime +7 -name "*.sql" -delete
恢复方式就一行:
bash复制cat backup.sql | docker exec -i pre-dish-mysql mysql -uroot -p$MYSQL_ROOT_PASSWORD pre_dish_db
这类调度系统的数据量一天几万条流水撑死了,mysqldump 完全够用,不需要做复杂的增量备份方案。
7. 复盘与优化:项目做完之后我重新审视的四个问题
项目从设计到上线整个流程走完,我对整个调度管控系统有了更深的理解。聊几个我认为值得所有做同类项目的人认真思考的问题。
7.1 调度算法的复杂度:从人工到自动的渐进路线
这个系统的调度算法我并没有写得很复杂,本质上是一个基于规则的"贪心 + 分批"算法。和真正的 APS(高级排产系统)相比,差距是非常大的。APS 通常要考虑多目标优化,比如最大化产能利用率、最小化换线时间、均衡各产线负载,还要用遗传算法或者线性规划来求解。我这个系统在毕设或者中小企业场景下已经够用,但如果你想把它真正用于生产环境,调度算法这块可以往约束满足问题的方向深挖,比如用 optaplanner 或者 google OR-Tools 来做更优的排程。
7.2 报表统计和数据分析的缺失
目前的系统只实现了基本的看板统计,比如按日期的订单量、产量、库存周转率。如果想要更好地发挥调度管控系统的价值,发货时效分析、产线利用率分析、物料消耗趋势预测这些报表功能是很有必要加上的。不过做报表要谨慎,不要一开始就堆图表,先把核心指标定义清楚,再考虑展示方式。我的建议是优先做三个指标:准时交付率、产线利用率、库存周转天数。这三个指标直接反映调度做得好不好。
7.3 权限控制的粒度是否够细
目前系统用的是 RBAC,角色到菜单级别的控制。但实际应用中,调度模块可能需要"数据级别"的权限控制,比如某些调度员只能看到自己负责的产线或者某些客户的数据。如果你的系统中角色更多、组织架构更复杂,建议用更细粒度的数据权限方案,MyBatis-Plus 的拦截器机制可以支持这种需求。我做这个项目的时候没有加数据权限,因为四个角色的权限边界还算清晰,数据量也不大,强行加数据权限反而会让代码更复杂。
7.4 前端交互体验的提升空间
调度系统最终是给人用的,如果交互不够顺手,调度员就宁可回到 Excel 去排产。这套系统的前端目前是比较传统的管理后台风格。真正好用的调度系统,排产界面应该是一个交互式的甘特图,拖拽式的调整工单日期和产线,所有的产能冲突用颜色高亮提示。这是目前这个项目的最明显短板,也是我下一阶段迭代优先会补的功能。前端可以用 dhtmlxGantt 或者 frappe-gantt 来快速实现一个可交互的排程视图。
我自己做完这套系统的最大感受是:调度管控系统的价值不在于代码有多复杂,而在于业务逻辑建模是否准确地把制造业和供应链的约束转化成了清晰的数据流转。只要需求池、工单、物料需求、库存流水这条主线理清了,整个系统的骨架就立住了,剩下的功能都是在这个骨架上的延伸。希望这套设计思路对也在做预制菜或者类似调度场景的同学有帮助。
