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 侧习惯 CRLFUNIX 侧习惯 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 条会被再计本文还有配套的精品资源点击获取