云计算百科
云计算领域专业知识百科平台

离散制造系统的数据建模:为什么流程模型套不上车间

一、先说结论:分岔发生在第一张表

做过流程行业系统的人转到离散制造,最容易踩的坑是「以为只是字段不同」。

真正不同的是主对象。

流程 ERP 的主对象是凭证(voucher)与物料(material):业务发生 → 凭证;物料移动 → 库存事务。生产执行被压缩成「生产订单 + 投料/产出」两个动作。

离散车间的主对象是工单与工单工序:一张工单 = 一个产品 + 一串工段 + 一个数量;执行落在「某道工序谁在什么时候做完了几件」。

这不是字段差异,是两种记账单位。记账单位不同,后面所有索引、约束、状态机、统计口径全都跟着不同。在这里插入图片描述

下面按表逐层拆,最后给三条迁移踩坑和一组性能注意。

二、工单:数量必须是「件」,且必须来自订单

流程系统的数量几乎都是 numeric(18,3) 这样的浮点,因为料是可称的。

离散的数量要绑件:

create table work_orders (
id uuid primary key,
tenant_id uuid not null,
order_id uuid not null, — 必须来自客户订单
order_number varchar(50) not null, — 工单号(业务主键)
part_no varchar(100), — 件号
product_name varchar(200),
quantity numeric(12,2) not null check (quantity > 0),
material varchar(100),
length numeric(12,3),
width numeric(12,3),
height numeric(12,3),
status varchar(20) not null default 'pending',
created_at timestamptz not null default now(),
unique (tenant_id, order_number)
);
create index idx_wo_tenant_status on work_orders (tenant_id, status);
create index idx_wo_order on work_orders (tenant_id, order_id);

两个约束值得单独讲。

order_id not null:工单不能凭空建。这条在数据库层就要卡住,因为一旦允许自由建工单,订单→工单的追溯链在第一环就断了,之后的报工、外协、对账全部悬空。

unique (tenant_id, order_number):单号在租户内唯一。跨租户可以重复(A 厂和 B 厂都可能有 WO20261027001),所以唯一键必须带 tenant_id 前缀——这是多租户系统最常见的一处疏漏。

数量为什么不用浮点?因为下游一整套计算都挂在「件」上:

  • 报工完成数量累加,必须是件
  • 工序计划工时是「小时/件」,乘数量才是总工时
  • 成本里的加工费按件数口径放大

一旦允许 0.5 件,报工记录会出现半件完工,库存和成本全部带零头,对账时谁都解释不清。

三、工序:不是工艺路线模板,是这张单实际要走的路径

流程系统的工序建模是「工艺路线模板(routing)+ 参数段」:一个产品绑一条路线,路线里的段是抽象的。

离散必须落到具体工单:

create table work_order_process (
id uuid primary key,
tenant_id uuid not null,
work_order_id uuid not null references work_orders(id) on delete cascade,
seq int not null, — 工段顺序,从 1 开始
process_id uuid not null, — 工段定义(含小时费率)
process_name varchar(100),
planned_hours numeric(10,3), — 计划工时(小时/件)
status varchar(20) not null default 'pending',
unique (work_order_id, seq)
);
create index idx_wop_wo on work_order_process (tenant_id, work_order_id, seq);

unique (work_order_id, seq) 这条约束表达的是「件内串行」:同一张工单的工序顺序号不能重复。

但它只能防重号,防不了乱序执行。真正的串行校验在应用层:

# 报工前置校验(伪代码)
def assert_process_order(wop, prev_wops):
if wop.status == 'pending' and any(p.status not in ('done', 'skipped') for p in prev_wops):
raise ConflictError("前道工序未完工,不能开工当前工序")

注意这里用 skipped 而不是直接删除:工序被跳过也是一条历史,删掉会让后续追溯断档(客户改需求不做热处理,是常态)。

件内串行、件间并行,是这个模型的第二个隐含约束。

不同工单(或同工单不同件)可以同时在不同工段上跑:A 单在磨、B 单在车、C 单在外协。流程系统的「工序段」模型表达不了这一层——它只有一条流水,没有「多张单同时卡在不同段」的概念。

四、报工:一行业务事实,四个字段各有脾气

离散的核心执行记录是报工。

create table work_report (
id uuid primary key,
tenant_id uuid not null,
work_order_process_id uuid not null references work_order_process(id) on delete cascade,
worker_id uuid,
equipment_id uuid,
started_at timestamptz,
completed_at timestamptz,
quantity numeric(12,2), — 本次完成数量
duration_hours numeric(10,3), — 完工时按 started/completed 自动算
allocated_hours numeric(10,3), — 多人工段的分配工时
source varchar(20) not null default 'manual', — manual / outsourcing
client_uuid uuid, — 客户端幂等键(离线补传用)
created_at timestamptz not null default now()
);
create unique index uq_report_client on work_report (tenant_id, client_uuid) where client_uuid is not null;
create index idx_report_wop on work_report (tenant_id, work_order_process_id);
create index idx_report_time on work_report (tenant_id, started_at);

四个字段的脾气:

duration_hours 是跨度,不是净加工时间。 它是 completed_at – started_at,中途停机、午休、去帮别的忙都算在里面。这是有意的定义:一台机床被某张单占用的时间,本身就是成本的一部分。要统计净加工时间得靠设备联网或分段报工。

allocated_hours 解决多人工段。 一道工序三个人干,工时总量要在人头上分。分配是独立动作,改一个人的分配数要校验其他人总和不超过这道工序的实际工时。

source 区分人工与外协自动驱动。 多入口写同一张工单时,来源标识是排查的第一把钥匙——数量对不上,先按 source 分堆,人工录入错误和外协对接错误,排查方向完全不同。

client_uuid 是幂等键。 车间弱网、PDA 离线补传是常态,同一条报工可能被重传三次。没有幂等键,补传就等于重复报工,数量翻倍。部分唯一索引(where client_uuid is not null)让历史数据不受影响,同时保证新数据不重复。

五、成本:只有两个字段,算法是确定的

最常见的误解是「系统会自动把料、工、外协三摊都归集」。

离散实现的成本表只有两摊:

create table work_order_costs (
id uuid primary key,
tenant_id uuid not null,
work_order_id uuid not null unique,
material_cost numeric(12,2) default 0,
processing_cost numeric(12,2) default 0,
loss_factor double precision not null default 1.0,
total_cost numeric(12,2) default 0,
note text,
updated_at timestamptz
);

算法:

material_cost = length × width × height(mm³) × density(g/cm³) × unit_price(元/kg) / 1e6 × loss_factor
processing_cost = Σ( planned_hours × process.hourly_rate × qty_factor )
qty_factor = work_order.quantity (per_piece 模式)
= 1 (total 模式)
total_cost = material_cost + processing_cost

实现片段(Python 风格,语义与真实实现一致):

def recalc(wo, wop_rows, material, mode, loss_factor):
# 料:体积 × 密度 × 单价 / 1e6 × 损耗系数
material_cost = Decimal("0")
if material and all([wo.length, wo.width, wo.height,
material.density, material.unit_price]):
vol_mm3 = float(wo.length) * float(wo.width) * float(wo.height)
kg = vol_mm3 * float(material.density) * float(material.unit_price) / 1_000_000.0
material_cost = Decimal(f"{kg * loss_factor:.2f}")

# 工:计划工时 × 工段费率 × 数量系数
qty_factor = wo.quantity if mode == "per_piece" else 1
processing_cost = Decimal("0")
for wop, rate in wop_rows: # rate 来自 process.hourly_rate
processing_cost += Decimal(
f"{float(wop.planned_hours or 0) * float(rate or 0) * qty_factor:.2f}")

return material_cost, processing_cost, material_cost + processing_cost

三点必须写进文档,否则三个月后一定有人问:

  • 料按体积算,不按实际领料。 锯口、夹头、试切用 loss_factor 补;它默认 1.0,要按厂里实际调。
  • 工按计划工时算,不按实际工时。 报价在产前,只有估的工时可用;实际工时是事后对照项。
  • 外协费用不在这张表里。 外协走对账链路。算完整直接成本要 total_cost + 外协对账确认金额。
  • 还有一个坑:重算通常是覆盖式的。

    update work_order_costs
    set material_cost = ?, processing_cost = ?, total_cost = ?, updated_at = now()
    where work_order_id = ? and tenant_id = ?;

    覆盖意味着历史成本不保留。要算「变更改掉了多少钱」,必须在变更前后各留一次数(导出、截图或写进变更单备注)。指望系统给你差额,是没有的——系统提供的是计算器,不是损益表。

    六、变更:独立单据 + 双快照,客户的改动不能直接改字段

    流程系统的变更通常是「修改单据字段 + 操作日志」。离散不行,因为变更会影响数量、工序、成本口径。

    离散的做法是两种变更单分治:

    — 客户订单变更(单号 OC{YYMM}{4位})
    create table customer_order_change (
    id uuid primary key,
    tenant_id uuid not null,
    change_no varchar(50) not null,
    order_id uuid not null,
    change_type varchar(20) not null, — delivery_date / cancel / force_cancel
    status varchar(20) not null default 'draft', — draft / executed / void
    original_data text, — 变更前 JSON 快照
    new_data text, — 变更后 JSON 快照
    change_detail text, — 人类可读说明
    reason text not null, — 原因必填
    executed_by varchar(36),
    executed_at timestamptz,
    unique (tenant_id, change_no)
    );

    — 工单变更(单号 CH{YYMM}{4位})
    create table work_order_changes (
    id uuid primary key,
    tenant_id uuid not null,
    change_no varchar(50) not null,
    work_order_id uuid not null,
    change_type varchar(20) not null, — delivery_date / quantity / process / cancel
    status varchar(20) not null default 'draft',
    old_value text,
    new_value text,
    reason text not null,
    unique (tenant_id, change_no)
    );

    — 工单变更明细:与工段结构对齐
    create table work_order_change_items (
    id uuid primary key,
    tenant_id uuid not null,
    change_id uuid not null references work_order_changes(id) on delete cascade,
    seq int not null,
    process_id uuid not null,
    planned_hours numeric(10,3),
    action varchar(20), — add / delete / modify
    old_planned_hours numeric(10,3),
    old_remark text
    );

    两个设计决定值得解释。

    客户订单变更只开放三类。 改交期、取消、强制取消。改数量属于危险变更,被挡在这个层级之外——一张订单的数量变化会连锁影响料、工时、外协和报价。这是有意收窄的入口,不是功能缺失。

    工单变更明细按工段一对一排。 action 三值 + old_planned_hours,这样成本才可能算出「差多少」。如果明细只记一句「工序有调整」,这张变更单等于废纸——车间不知道改哪道,成本算不出差额。

    另外,force_cancel(强制取消)要级联取消其下 pending / in_progress 的工单,但外协、发货、对账不自动动。这条边界必须写死在文档里:级联做多了会连带毁掉下游的真实业务记录。

    七、迁移:从流程系统搬数据,三个必踩的坑

    做过一次流程 ERP → 离散 MES 的数据迁移,会踩三个坑。

    坑一:数量带小数。 流程系统里的数量是浮点,搬过来之后 quantity 出现 0.5、1.25。清洗规则要提前定:要么向上取整,要么单独立一条「非法数据」台账人工核。悄悄 round 是最坏的选择。

    坑二:工序没有 seq。 流程系统的工艺段是模板,搬到工单上要生成实际的工单工序。如果模板里段的顺序不明确,生成的 seq 会乱,串行校验全部失效。迁移前要人工确认一遍段序。

    坑三:料号一对多。 流程系统一个料号对应一个规格;离散厂的历史数据里,同一个料号常常被用在多个件号上(因为当年没料号就随手填了)。迁移映射表要么按「料号 + 件号」建,要么承认这部分成本只能按估算。

    这三个坑的共同点:都不是技术难点,是数据质量。 清洗脚本好写,判断标准难定,所以必须业务方参与确认,不能全交给开发。

    八、多租户与性能:两条容易漏的

    多租户隔离与业务模型正交,但有两处容易漏:

  • 所有唯一键都要带 tenant_id(单号、料号、图号在租户内唯一,跨租户会重复)
  • 所有统计查询的 where 必须先带 tenant_id,否则索引走不上,还可能跨租户污染
  • 成本统计这类聚合查询尤其要注意:

    — 租户前置 + 时间范围 + 只统计已完工
    select count(*), coalesce(sum(c.total_cost), 0)
    from work_order_costs c
    join work_orders w
    on w.id = c.work_order_id and w.tenant_id = c.tenant_id
    where c.tenant_id = :tid
    and w.status = 'completed'
    and w.completed_at >= :from
    and w.completed_at < :to;

    性能上离散系统有两个热点:

    报工写入。 车间集中报工(午休、下班)会形成写入尖峰,索引不要建太多,work_report 上两三个足够(按工序查、按时间查)。

    看板查询。 看板是高频只读聚合,最好做一层物化视图或缓存,别每次都跑全表 count。

    九、模型对不对口,看四个约束

    给做选型评审或代码评审的人四条硬判据:

  • 工单能否只表示一件活(数量下限为 1,不要求标准产品)
  • 工序能否挂在工单上、按 seq 串行推进,且每道工序可独立报工
  • 外协能否挂在「工单 + 工序」这一对上,而不是挂在批次上
  • 成本能否按单查询,且能明确说出「含什么、不含什么」
  • 四条里任一条不成立,这套模型就不是离散模型,用它管车间必然多一层翻译。

    上面这些约束不是纸上推演——它们来自一套真实在跑的离散 MES 的数据结构(摩杰斯的 PMS5),字段名、默认值、约束都能逐条对上,包括「客户订单变更只开放三类」这种看起来别扭、实际是护栏的设计。

    FAQ

    Q:为什么不用「批次」做主轴,把料和活串起来?
    A:批次适合同质料的质量追溯(流程行业)。离散的非标件没有稳定批次谱系:一件可能来自三批料,一批料可能做五个图号。按批次管,追溯不出有用的信息。真要做批次级追溯,得引入批次号、先进先出、有效期这一整套,成本和维护量是另一个量级。

    Q:工时能不能直接用工时卡/设备采集替代报工?
    A:能补,但替代不了。报工记录的是「这张单的这道工序完成了多少件」,设备采集通常只有运行时长,没有件数与工序归属。两者互补:设备采集校准时长,报工提供件数与归属。

    Q:成本口径为什么不能两套并存(按单 + 按批)?
    A:可以并存,但必须先明确哪一套是决策口径。两套并行时最常见的失败是报价用按单、复盘用按批,然后两边数永远对不上。技术上只是 qty_factor 一个开关,管理上是一个态度问题。

    Q:这类模型能承载多租户 SaaS 吗?
    A:能。多租户的行级隔离(tenant_id)与业务模型正交,只要所有查询都带租户上下文、唯一键都带租户前缀,模型本身不受影响。真正要小心的是聚合查询和导出——这两处最容易漏掉租户条件。

    Q:离线报工的数据怎么保证不重复?
    A:客户端生成 client_uuid,服务端用它建部分唯一索引。补传时先查后插,或者直接依赖唯一索引冲突并返回已有记录。没有幂等键的离线补传,迟早会出现重复报工。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 离散制造系统的数据建模:为什么流程模型套不上车间
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!