MES接口用共享文件与幂等设计实现可靠批处理集成
简介:面向 SAP R/3 与制造执行系统(MES)集成项目的设计参考文档,聚焦 MELEBUS-BMAS 模板下的 MES 接口附加程序说明,适合实施顾问、接口开发人员及制造业信息化项目组在原型搭建与接口验证阶段查阅。压缩包仅含 1 个 doc 文件,大小约 178KB,内容围绕制造指图数据抽取、实绩数据计上的系统间接口展开,并给出接口文件一览与规格、ZMAF001、ZMAF005 程序说明、参数设定及 R/3 与 MES 间排他控制等章节。读者可据此理解接口适用范围、前提条件、处理流程与文件定义,用于方案设计、开发对照和联调排错。已有 2668 人学习。
1. 先说结论:MES 接口为什么可以只是一堆共享文件
制造业里有个反直觉的事实:一条每天处理大量工单的产线,MES 接口的物理形态可能只是共享目录里的四个文本文件,外加一个空文件当锁。没有消息队列,没有 REST,没有长连接。这套 MES 接口设计在「一到两次每天」的批处理节奏下,反而比实时接口更不容易出事故——因为每一步都能被肉眼看见、被手工修复。它解决的是一类很具体的工程问题:ERP 侧的制造指图(生产订单)如何安全地传到车间,车间的投入与完工实绩又如何安全地反馈给 ERP,并且在两套异构操作系统、两种换行码、没有中间件的约束下做到不丢、不重、不乱序。搞清楚这套机制,比学会调一个 API 更能理解系统集成的本质。
2. 接口的物理形态:四个文本文件如何承载一条完整生产链路
系统集成的第一课往往是「用什么协议」,但这套接口的设计者把问题倒过来问:在批处理、低频次、跨组织边界的场景里,协议越轻越可靠。文本文件恰好是最轻的协议——任何语言都能读,任何编辑器都能看,出错时不需要抓包,直接打开文件就能知道对面到底收到了什么。下面把四个文件、两对数据流和一组环境参数拆开看。
2.1 下行与上行:两对文件的分工
下行方向是制造指图抽出:ERP 侧把满足条件的指图头信息和指图对应的组件物料明细分别写进两个接口文件。头文件和明细文件分离,是因为一条指图下的组件行数量不定,拆开可以避免单文件过大,也让读取方可以只处理头、稍后再处理明细。上行方向是实绩计上:MES 侧把投入实绩(消耗了哪些组件、多少数量)和完成实绩(完工数量、不良数量、预计完工日)写进另外两个文件,ERP 侧读取后回写为作业实绩。
| 文件 | 方向 | 内容 | 写入方 | 读取方 |
|---|---|---|---|---|
| 指图头文件 | ERP → MES | 指图号、类型、开始日期、状态 | ERP 抽出程序 | MES 导入程序 |
| 组件明细文件 | ERP → MES | 指图号、组件编码、需求数量 | ERP 抽出程序 | MES 导入程序 |
| 投入实绩文件 | MES → ERP | 指图号、组件编码、消耗数量 | MES 导出程序 | ERP 计上程序 |
| 完成实绩文件 | MES → ERP | 指图号、完工/不良数量、确认区分 | MES 导出程序 | ERP 计上程序 |
每个文件都带唯一键,这一点在后面的幂等性讨论里会反复出现。键的构成通常是「指图号 + 行项目 + 业务日期」这类能唯一标识一条业务记录的组合,它既是去重的依据,也是读取方判断「这条是不是重发的」的唯一线索。文件本身是纯文本,字段分隔符固定,不做压缩、不做加密,原因很简单:这套接口的首要目标是可运维,而不是性能。
文件格式还有两个隐含约束:一行一条记录,字段顺序固定,不允许跨行。固定顺序意味着读取方可以按位置解析,不需要字段名,代价是任何字段增减都必须两端同时改。跨行禁止则保证了读取方可以按行切分、逐行处理,不会出现「读了一半的记录」。这些约束看起来原始,但它们让整个接口在没有任何 schema 校验的情况下依然能稳定运行很多年。
2.2 变体参数:把环境差异从代码里赶出去
同一份抽出程序和计上程序,要部署到不同现场,而现场的差异往往集中在几个点上:操作系统是 Windows 还是 UNIX、文件放在哪个共享路径、日志保留多少天、抽出时按哪种方式判重、实绩按累计还是按日别计上。把这些差异全部塞进代码,程序就没法复用;这套接口的做法是把它们抽成变体(variant)参数,运行时选择。
常见的参数项包括换行码(CRLF 还是 LF)、接口文件的逻辑路径、日志文件的保留期限、抽出条件的日期窗口、以及实绩计上的种别。逻辑路径这一项尤其关键:程序里不写死物理路径,而是通过逻辑路径间接指向共享目录,换机器时只改变量不改程序。日期窗口则决定了抽出程序「往前看几天」,这个值设得太大,会捞出一堆历史指图;设得太小,夜班产生的指图可能第二天才被看到。
提示:变体参数不是「配置越多越好」。每增加一个参数,就多一种现场组合,也就多一类「我这里能跑你那里不能跑」的问题。这套接口把参数收敛在环境差异和业务节奏两类,其余一律固化,是值得借鉴的边界感。
从源码结构上看,抽出程序和计上程序都遵循「读取参数 → 校验前置条件 → 处理数据 → 写日志」的基本流程。参数读取放在最前面,一旦参数缺失或路径不可达,程序在碰任何业务数据之前就退出,避免处理到一半才发现环境不对。这种「先失败」的设计在批处理里比「尽力而为」更安全,因为批处理没有用户盯着,半途而废的后果往往比直接不跑更糟。程序里承载这两段逻辑的通常是以 Z 开头的自定义命名空间程序,一套负责抽出、一套负责计上,彼此不共享全局变量,只通过文件和参数耦合。
2.3 透传与换行码:为什么接口不做字符集转换
接口文件在两端之间是透传的——写出去是什么字节,读进来就是什么字节,中间不做代码转换。这个选择有明确的代价和收益。收益是链路短、故障点少,文件可以用任何工具打开核对;代价是字符集和换行码必须由两端事先约定,一旦一端按 UTF-8 写、另一端按本地编码读,中文或日文物料名就会变成乱码,而且这种乱码不会在传输层报错,只会安静地污染数据。
换行码的处理稍微聪明一点:它被做成参数。Windows 侧习惯 CRLF,UNIX 侧习惯 LF,如果写死一种,另一端读进来的每一行都会带一个多余的\r,字段解析随之错位。把它参数化,等于承认「文件格式是环境的一部分」,而不是程序的一部分。下面这段伪代码展示了参数化读文件时的关键判断,重点不在语法,而在「读之前先确认格式」的顺序。
" 读取变体参数:换行码类型与逻辑路径 DATA: lv_eol_type TYPE string, lv_logical_path TYPE string, lt_lines TYPE STANDARD TABLE OF string. " 1. 先取参数,参数缺失直接退出,不碰业务数据 CALL FUNCTION 'GET_VARIANT_PARAMS' EXPORTING iv_variant = iv_variant_name IMPORTING ev_eol = lv_eol_type ev_path = lv_logical_path EXCEPTIONS not_found = 1. IF sy-subrc <> 0. " 参数缺失属于配置错误,写开始/结束日志后终止 PERFORM write_start_end_log USING 'PARAM_MISSING'. EXIT. ENDIF. " 2. 按换行码类型切分,避免把 \r 带进字段 CALL FUNCTION 'READ_SHARED_FILE' EXPORTING iv_logical_path = lv_logical_path iv_eol_type = lv_eol_type " 'CRLF' 或 'LF' TABLES et_lines = lt_lines EXCEPTIONS file_not_found = 1 read_error = 2. " 3. 只有读到数据才进入业务处理,空文件视为本批次无任务 IF lt_lines IS INITIAL. PERFORM write_log USING 'NO_DATA'. EXIT. ENDIF.这段骨架里有两个顺序值得注意。第一,参数读取在文件读取之前,文件读取在业务处理之前,任何一步失败都不会留下「处理了一半」的中间状态。第二,空文件被当作正常情况而不是错误,因为批处理里「今天没有新指图」是完全合法的结果,如果把它当异常处理,日志里会充满误报,真正的故障反而被淹没。参数iv_eol_type传给底层读文件函数,由它决定切分逻辑;iv_logical_path则通过系统层面的逻辑路径映射到物理路径,程序本身对盘符和挂载点一无所知。
这套透传加参数化的组合,本质上是把「格式约定」从代码里挪到了部署文档里。它不优雅,但极其透明:出问题时,运维只要拿到文件、看一眼换行码和编码,就能判断问题出在写端还是读端。相比之下,一个做了隐式转换的接口,出问题时你很难说清乱码是在哪一步产生的。
3. 排他控制:一个空文件如何成为跨系统互斥锁
共享目录最大的风险不是读写慢,而是两端同时动同一批文件。ERP 侧正在追加指图,MES 侧同时读到一半并删除,结果是数据丢失;反过来,MES 正在写实绩,ERP 侧同时在读,读到的是半条记录。这套接口没有用数据库锁,也没有用文件系统的强制锁,而是用一个更简单的办法:管理文件。它的逻辑简单到近乎原始,但正因为简单,跨操作系统、跨文件系统都能用。
3.1 管理文件即临界区:创建与删除的生命周期
管理文件分两个,一个管指图处理,一个管实绩处理。规则只有两条:处理开始前检查管理文件是否存在,不存在才创建并开始处理;处理完成后删除管理文件。存在即锁定,删除即释放。这是一个标准的临界区模型,只是把信号量换成了文件的存在性。
为什么用文件存在性而不是文件锁?因为文件锁在不同操作系统、不同文件系统(尤其是网络共享)上的语义并不一致,有的 advisory lock 形同虚设,有的在进程崩溃后不会自动释放。而「创建一个文件」这个动作在几乎所有文件系统上都是原子的,创建成功就意味着抢到了锁。代价是这个锁没有持有者信息,也没有自动过期,一旦持锁进程崩了,锁就永远留在那里。
从时序上看,一次完整的处理是这样的:检查管理文件 → 不存在则创建 → 执行抽出或计上 → 写日志 → 删除管理文件。任何一步异常退出,管理文件都不会被删除,于是下一次调度进来,看到管理文件存在,直接跳过本轮。这既是保护,也是隐患——保护的是不会在上一轮没结束时重入,隐患是上一轮如果已经死了,后面所有轮次都会被这个残留文件挡住。
注意:管理文件的创建必须放在所有业务动作之前,删除必须放在所有业务动作之后且不受异常影响。顺序反了,排他就形同虚设。
还有一个容易被忽略的细节:删除管理文件这个动作本身也可能失败,比如网络瞬断、权限被改。如果程序把「删除失败」当成普通警告继续往下走,锁就留下了;更糟的是,下一轮可能在锁没释放的情况下又创建成功(某些网络文件系统对删除有延迟),造成两个实例同时运行。所以删除动作要有返回值检查,失败就写日志并让本轮以非正常结束,交给监控去发现,而不是假装成功。
3.2 单写多读与唯一键去重
排他只解决了「两端不同时动」,没解决「同一批数据被处理两次」。这套接口用所有权划分来补:写入方永远只追加,不修改、不删除;读取方处理完成后才删除文件(或文件中的记录)。这样一来,文件在读取方确认处理成功之前始终存在,即使读取方中途崩溃,下一轮还能重新读到,不会丢数据。
追加模式带来一个副作用:同一条记录可能被写两次。比如写入方写完文件后、还没来得及记录「已发送」就崩了,下一轮会再写一遍。如果读取方不判重,就会重复计上。于是每个接口文件都带唯一键,读取方在处理每条记录前先用键查一下「这条是不是处理过」,处理过就跳过。这个判重不是靠文件内的去重,而是靠读取方的处理记录,因为文件可能已经被删了。
这三个机制——管理文件互斥、追加写、唯一键判重——组合起来,构成了一个没有事务、没有中间件的「至少一次」投递语义。它不保证恰好一次,但保证不丢;重复由唯一键拦截,丢失则几乎没有可能。在批处理场景里,这个取舍是对的:重复可以事后清理,丢失往往无法追溯。
读取方删除文件的时机也要讲究。常见做法是「一条记录处理成功就删一条」,这样即使中途崩溃,已处理的记录不会再被读到,未处理的留在文件里。但如果文件格式是整批写入、整批删除,那删文件就必须放在整批处理成功之后,并且删除前要确认工作数据库已经提交。两种做法的选择标准是:单条记录能不能独立判定成功。能,就逐条删;不能,就整批删加工作库兜底。
3.3 进程崩溃后的锁残留与老化处理
管理文件最大的问题是残留。设想一个场景:计上程序在写工作数据库时因为表空间满而异常终止,管理文件没被删除。下一轮调度进来,看到管理文件存在,认为「对面还在处理」,直接跳过。再下一轮,还是跳过。如果没有人工介入,这个接口会安静地停摆好几天,而日志里只有「检测到管理文件,跳过处理」这种看起来很正常的信息。
处理办法有两类。一类是运维层面的:监控管理文件的最后修改时间,超过一个处理周期还没变化,就告警并人工确认后删除。另一类是程序层面的:在管理文件里写入持锁进程的标识和加锁时间,下一轮进来时先判断「锁是不是过期了」,过期则强制接管。后一种更自动化,但也更危险——如果判断逻辑本身有 bug,可能把正在处理的锁抢走,造成真正的并发。这套接口选择了前一种,把判断权交给运维,程序只负责如实记录。
我见过更隐蔽的一种残留:不是进程崩溃,而是处理时间超过了调度间隔。比如抽出程序正常情况下跑十分钟,某天因为指图数量暴涨跑了两个小时,而调度器每小时拉起一次。第二小时的实例看到管理文件存在,跳过;第三小时如果第一个实例还没结束,继续跳过。这不算 bug,但会让「接口延迟」看起来像「接口故障」。要区分这两种情况,日志里的开始时间和结束时间缺一不可,这也是为什么这套接口坚持写两种日志:开始/结束文件记录每轮的起止,明细日志记录每条记录的处理结果。监控上至少要看三个指标:管理文件的存活时长、每轮处理的记录条数、以及接口文件的积压行数。三者一起看,才能区分「没数据」「处理慢」和「卡死了」。
4. 幂等性:抽取与计上为什么必须能重跑
批处理程序的最高要求不是快,是能重跑。网络抖动、文件被占用、数据库连接超时,任何一个小故障都可能让一轮处理半途而废,而调度器不会知道上一轮死在哪,它只会在下一个时间点再拉起一次。如果程序不是幂等的,重跑就意味着重复写、重复计上、重复发料。这套接口在抽取侧和计上侧各用了一套机制来保证「重跑无害」,思路不同,但目标一致。
4.1 两种抽取管理方式:用户状态 SEND 与日志比对
抽出程序的核心问题是:怎么知道一条指图已经发过了。这套接口给了两种可选方式,运行时二选一。
第一种是用户状态法。ERP 的指图对象上可以挂用户自定义状态,抽出成功后把SEND状态置为开启,下一轮抽出时只看状态没开的指图。这种方式的优点是状态跟着业务对象走,查一条指图就能知道它发没发过,不依赖外部文件;缺点是状态是「当前值」,一旦有人手工改回去,或者状态更新失败但文件已写,就会不一致。
第二种是日志比对法。维护一个发送日志文件,记录已发送的指图号,抽出前先比对,日志里有的就跳过。优点是日志是追加的,历史可追溯;缺点是日志会越来越大,而且日志和接口文件是两份数据,存在「文件写了但日志没写」的窗口。
两种方式都指向同一个原则:判重依据必须落在「抽出成功之后」,而不是「抽出之前」。如果先标记再写文件,写文件失败就会导致指图永远发不出去,这比重复发送严重得多。所以顺序永远是先写接口文件,再更新状态或日志,中间崩溃的后果是「可能重复」,由接收方的唯一键兜底。
" 抽出主循环:先写文件,后更新状态,顺序不可颠倒 LOOP AT lt_orders INTO ls_order. " 1. 四重过滤:日期窗口、系统状态、是否取消、是否已发送 IF ls_order-start_date < lv_date_from OR ls_order-start_date > lv_date_to. CONTINUE. ENDIF. IF ls_order-system_status <> 'REL'. " 必须是已发行 CONTINUE. ENDIF. IF ls_order-deleted = abap_true. CONTINUE. ENDIF. IF ls_order-user_status_send = abap_true AND lv_mgmt_type = 'STATUS'. CONTINUE. ENDIF. " 2. 先追加写接口文件,写成功才继续 PERFORM append_to_if_file USING ls_order. IF sy-subrc <> 0. " 写失败立即退出,不更新任何状态,保证下轮可重试 PERFORM write_log USING 'WRITE_FAILED'. EXIT. ENDIF. " 3. 再更新判重依据,两种方式二选一 IF lv_mgmt_type = 'STATUS'. PERFORM set_user_status_send USING ls_order-order_no. ELSE. PERFORM append_send_log USING ls_order-order_no. ENDIF. ENDLOOP.这段循环里最关键的是第 2 步和第 3 步的顺序。注释里写「顺序不可颠倒」不是修辞:如果先置状态再写文件,写文件失败的那条指图在状态上已经「发过」,下一轮会被过滤掉,结果是永久丢失。反过来,先写文件再置状态,最坏情况是文件写成功、状态没置上,下一轮重复写一次,接收方用唯一键丢掉重复记录即可。两个方向的故障代价不对等,所以选择「允许重复、拒绝丢失」。
四重过滤里的日期窗口和系统状态是业务约束,SEND状态是技术约束。把它们放在同一个CONTINUE链里,意味着任意一条不满足就跳过,逻辑清晰但效率一般——每轮都要全量扫一遍候选集。在指图量不大的现场这没问题,量大的话通常会先在数据库层用WHERE收窄,再进循环做状态判断。注意日期窗口比较的是「着手日」,不是「创建日」,这两个日期在计划变更频繁的现场可能差好几天。
4.2 实绩侧的工作数据库预写
计上侧的风险比抽出侧更高,因为抽出重复只是多一个文件记录,计上重复是实打实的数量翻倍。这套接口的对策是「工作数据库预写」:读取接口文件后,先把待计上的数据写进一张工作表,再从工作表写入正式的实绩,全部成功后才删除接口文件。
工作表的作用是隔离异常终止。设想程序读到第 500 条时数据库连接断了,如果直接写正式实绩,前 499 条已经生效,接口文件还在,下一轮从头读,前 499 条会被再计
本文还有配套的精品资源,点击获取