☰
DELFOR EDI报文解析:从EDIFACT天书到JSON结构化数据
2026/10/14 5:07:31 网站建设 项目流程

做过EDI对接的朋友应该都有同感:客户那边一封邮件过来,附上一个AS2传完的DELFOR报文文件,打开一看就是密密麻麻的一串文本,段尾一个单引号,字段之间用加号和冒号隔开。某个Tier 1汽车零部件客户发给我方的DELFOR,每周至少来一份,里面装的是未来12周的滚动需求预测,我们的MES和生产计划系统却只认JSON。这篇文章就把我从这种“天书”到结构化JSON的完整拆解过程写清楚,包括DELFOR的段结构、关键字段的业务含义、转换方案的落地代码,以及那些标准文档里从来不会明说的坑。不管你是做EDI二次开发、供应链系统集成,还是刚接手客户报文解析,这篇文章应该都能让你少走不少弯路。

1. DELFOR 是供应链里的“远期预测协议”,不是订单

1.1 一个DELFOR里到底装着什么业务

DELFOR全称是Delivery Forecast,直译就是“交付预测”。它在汽车供应链里扮演的角色,是客户(买方)定期向供应商(卖方)发布的一版滚动物料需求计划。通常以周为单位滚动,比如本周发布未来12周的预测,下周再发布未来13周的预测,窗口不断往前推。供应商拿到这份预测后,用来做三件事:一是备料,确保长周期物料能提前下单;二是排产,把未来几周的产能提前锁住;三是算安全库存,因为预测是滚动变化的,有时候这周还在计划里的量,下周就可能被砍掉。

注意,DELFOR本身并不是“订单”,所以它不产生法律意义上的采购承诺。真正下单的动作发生在DELVRY(发货通知、交货指令)或ORDERS(采购订单)里。但是对很多制造业供应商来说,生产和物料准备的提前期远大于订单前置时间,所以必须依赖DELFOR来做中期计划。

1.2 DELFOR、ORDERS、DELVRY 到底怎么区分

很多刚接触EDI的人会把几种常见报文搞混,这里用一句话理清楚:

  • DELFOR:远期预测,告诉你未来有多少需求,供备料和排产用。
  • ORDERS:正式采购订单,告诉你“我要买这个、买多少、什么价钱”。
  • DELVRY:发货交付指令,告诉你“这批货给我发多少、哪天到”。

一个典型业务流是:客户每月/每周发DELFOR给出预测,临近交付时再发ORDERS或DELVRY确认实际采购数量,供应商按确认的发货指令出货。在EDI解析开发中,必须区分清楚:DELFOR里看到的数量不能直接当作订单量去扣库存,但可以根据它的“确认计划”部分来锁定产能。

1.3 哪些人最需要读懂这篇内容

如果下面的描述中有一条戳中你,那这篇文章就是写给你的:

  • 你在做EDI平台对接,某天收到一封邮件说“客户使用DELFOR报文,需要解析入库”。
  • 你在做MES/WMS/ERP集成,上游给到你的是一个EDI原始文件,但业务系统只接受JSON、XML或数据库表。
  • 你是供应链计划或采购工程师,经常要用EDI里的预测数据算产能,却不知道哪一段字段代表“确认数量”。

我在项目里最深的体会是:EDI报文的解析难度不在语法,而在对业务语义的判断。语法只要认真看标准都能写出来,但“这个限定符代表什么”“这种组合数据重复多少次代表什么含义”,必须结合业务反复校验。

2. 拿到样本报文后,先按 EDIFACT 语法把骨架拆出来

2.1 先搞懂分隔符逻辑

EDIFACT是UN/EDIFACT国际标准的报文格式,BorgWarner这类国际化Tier 1客户用的基本都是这套语法。它的核心规则其实很简单,记住三种符号就够:

  • 段终止符:单引号('),每段结尾都有一个。
  • 数据元素分隔符:加号(+),段名和各元素之间用加号分开。
  • 复合数据元素内分隔符:冒号(:),同一个复合元素内部多个成分用冒号分开。

比如一行DTM+137:20230106:102',拆开看是:段名DTM,后面复合数据元素“137:20230106:102”,“137”是日期时间限定符,“20230106”是具体日期,“102”是日期格式代码(表示YYYYMMDD)。理解了这个基本逻辑,后续所有解析工作都是在这个规则上做文章。

2.2 一份典型DELFOR示例(简化版)

为了说明问题,我把实际项目中拿到的客户样报做脱敏和精简,整理成下面这份可读性较高的DELFOR片段。它包含了消息头、参与方、参考号和两行产品的预测计划:

UNB+UNOC:3+BUYER01+SUPPLIER99+230106:0830+20230106000001' UNH+000001+DELFOR:D:96A:UN' BGM+51+FORECAST_REF_001+9' DTM+137:20230106:102' NAD+BY+123456789::92++某客户公司名' RFF+AAG:FORECAST_REF_001' LIN+1++PARTNO-A:BP' PIA+5+PARTNO-A:BP' QTY+113:5000:PCE' SCC+1' DTM+2:20230109:102' DTM+61:20230115:102' QTY+113:6000:PCE' SCC+1' DTM+2:20230116:102' DTM+61:20230122:102' LIN+2++PARTNO-B:BP' PIA+5+PARTNO-B:BP' QTY+113:3000:PCE' SCC+1' DTM+2:20230109:102' DTM+61:20230122:102' UNT+17+000001' UNZ+1+20230106000001'

很多第一次接触的人看到这个文件,第一反应是“这不就是一堆字符串吗?我按行切割不就行了?”但实际上问题没这么简单。这段示例里有两个产品PARTNO-A和PARTNO-B,PARTNO-A下面出现了两组QTY和SCC,代表两个预测窗口。如果你不区分上下文,就会把第二个窗口的QTY错误地绑定到产品上,丢失“窗口”信息。

2.3 段组是什么,为什么它决定了解析顺序

EDIFACT报文里,几种相关的段会组合成“段组(Segment Group)”。DELFOR的典型结构大致是:

  • 消息头:UNH + BGM + DTM
  • 参与方组:NAD + RFF
  • 行项目组:LIN + PIA + QTY + 计划组(SCC + DTM + QTY)

段组的最大特点是:允许重复。一个LIN下面可以重复出现多组SCC/DTM/QTY,分别代表不同的预测周期。这种“嵌套重复”结构用简单的按行切割是无法正确表达的,这也是为什么解析逻辑必须设计成“状态机”,而不是“正则表达式大合集”。

你从客户那边拿到的实际报文,段组可能更多、更复杂,比如还会有TDT(运输)、LOC(地点)、GIN(货物标识)等。但是不管怎么变,骨架逻辑一致:先分段,再识别段名,再根据当前所处的上下文把字段归位。

3. 字段级说明:一段一行看清业务语义

3.1 控制段与消息头:UNB、UNH、BGM、DTM

先看报文最前面的几个段。UNB是交换头,它定义了整个EDI交换的发送方、接收方、交换控制号和时间。UNH是消息头,表示这条消息的类型是DELFOR,版本是D.96A。BGM是业务消息开始段,里面写着消息类型代码“51”和业务参考号。

这里的字段对系统集成很重要。UNB里的发送方/接收方,通常用来做文件名或路由标记;BGM里的参考号,则可以作为JSON里的消息唯一键。举个例子,如果客户一周内发了三份DELFOR,三份的BGM参考号不一样,那它们就是三份独立的预测,必须分开保存。DTM+137:20230106:102表示文档创建日期是2023年1月6日,这部分信息在审计追溯时很关键。

3.2 参与方与单据参考:NAD、RFF

NAD段用来标识参与方。NAD+BY+123456789::92++某客户公司名'这行里,“BY”表示参与方角色是买方(Buyer),紧接着是买方编码和编码机构限定符“92”(表示由买方或卖方分配的代码)。在JSON里应该对应成一个买方对象,包含编码、编码机构、名称。

RFF段是参考号段。RFF+AAG:FORECAST_REF_001'里的“AAG”表示这是预测参考号,后面的值就是这个预测文件的业务编号。一个DELFOR文件里可能有多个RFF,比如订单号、合同号、计划号等,所以限定符必须解析出来,不能只取数值。

3.3 行项目与预测计划:LIN、PIA、QTY、SCC、DTM

这是整个报文里业务信息密度最大、也是最容易出错的区域。

  • LIN代表一行产品。LIN+1++PARTNO-A:BP'表示第一行,零件号是PARTNO-A,“BP”表示这个零件号是买方(客户)的零件号。
  • PIA是附加产品标识。如果LIN里没有零件号,PIA会补充;即使LIN里已有,PIA也常出现,用于多个产品标识互相关联。
  • QTY是数量。QTY+113:5000:PCE'表示数量限定符113下的预测数量是5000,单位PCE(个)。不过要注意,不同客户的6063数量限定符可能不同,有的用“1”表示交货数量,有的用“113”表示预测数量,不能只看后面的数字就下结论。
  • SCC表示计划状态。SCC+1'里的“1”是计划状态代码,含义在不同客户间有差异,常见解释是“1=确认计划”或“2=参考/计划性预测”。如果下游计划系统对“确认”和“参考”的处理策略完全不同,这个字段就必须准确解析。
  • DTM跟在SCC后面,表示计划周期的时间窗口。DTM+2:20230109:102'和DTM+61:20230115:102'分别表示该计划窗口从1月9日开始、到1月15日结束。

这里最关键的逻辑是:一行产品下面可能有多组“SCC+DTM+QTY”,每组就是一个独立的预测时间窗口。解析时必须把这一组绑定到当前LIN下,而且窗口起止日期必须成对处理。

3.4 字段映射速查表:从段元素到JSON

为了编码方便,我把常用的段元素和JSON字段做了映射:

EDI位置示例值业务含义目标JSON字段
UNB+UNOC:3BUYER01 / SUPPLIER99交换发送方/接收方interchange.sender / interchange.receiver
UNH000001消息控制参考号message.referenceNo
BGM+51FORECAST_REF_001业务文档类型/参考号message.type / message.businessNo
DTM+13720230106:102文档创建日期message.documentDate
NAD+BY123456789::92买方编码header.buyer.code
RFF+AAGFORECAST_REF_001预测参考号header.forecastReferenceNo
LIN1行项目序号lines[].lineNumber
PIA+5PARTNO-A:BP附加买方零件号lines[].buyerPartNo
QTY+1135000:PCE预测数量/单位lines[].quantities[].value / unit
SCC+11计划状态代码lines[].schedules[].statusCode
DTM+220230109:102窗口开始日期lines[].schedules[].startDate
DTM+6120230115:102窗口结束日期lines[].schedules[].endDate

这张表是我在项目里给下游计划系统对接时的基准。每个客户都会有细微差别,但整体骨架可以先按这个设计。

4. 用状态机把 EDI 转成 JSON:完整落地实现

4.1 为什么状态机比“按行匹配”更可靠

我见过不少刚入门的开发方案,是先按段名把DTM、QTY全抽出来,然后靠正则去猜“哪个DTM属于哪个产品”。这种方案在小样本测试时能跑通,一旦报文里多了个别的段、某个行下窗口数变化,数据就错位了。

正确做法是维护一个解析状态:当前处于“消息头”还是“行项目”还是“预测窗口”。每读到一个段,根据当前状态决定字段归属。这本质上就是一个有限状态机。它天然能处理段组重复的问题:看到LIN就新建一行产品,看到SCC就新建一个窗口,看到QTY就把它放进当前窗口或当前行。

4.2 解析主流程实现(Python)

下面是简化但可直接运行的解析思路。第一步是按段终止符切分:

from typing import List def split_segments(raw_text: str) -> List[str]: segments = [] buf = [] for ch in raw_text: if ch == "'": segments.append(''.join(buf).strip()) buf = [] else: buf.append(ch) return [seg for seg in segments if seg] def get_tag(segment: str) -> str: return segment[0:3]

然后是核心状态机。为了保持代码可读性,这里只保留了DELFOR最关键的几个段:

from typing import List, Dict, Any def parse_delfor(raw_text: str) -> Dict[str, Any]: segments = split_segments(raw_text) msg: Dict[str, Any] = {"header": {}, "lines": []} current_line: Dict[str, Any] | None = None current_schedule: Dict[str, Any] | None = None for seg in segments: tag = get_tag(seg) body = seg[3:] parts = body.split('+') if tag == "UNB": msg["interchange"] = { "sender": parts[1] if len(parts) > 1 else "", "receiver": parts[2] if len(parts) > 2 else "", } elif tag == "UNH": fields = parts msg["message"] = { "referenceNo": fields[0] if len(fields) > 0 else "" } elif tag == "BGM": msg["header"]["businessNo"] = parts[1] if len(parts) > 1 else "" elif tag == "DTM": dtm_parts = parts[0].split(':') qualifier = dtm_parts[0] if len(dtm_parts) > 0 else "" value = dtm_parts[1] if len(dtm_parts) > 1 else "" if current_schedule is not None: if qualifier == "2": current_schedule["startDate"] = value elif qualifier == "61": current_schedule["endDate"] = value elif qualifier == "137" and current_line is None: msg["header"]["documentDate"] = value elif current_line is None: if qualifier == "137": msg["header"]["documentDate"] = value elif tag == "NAD": msg["header"]["partnerRole"] = parts[0] if parts else "" if len(parts) > 1 and parts[1]: msg["header"]["partnerCode"] = parts[1].split(':')[0] elif tag == "RFF": pass elif tag == "LIN": current_line = { "lineNumber": parts[0], "buyerPartNo": "", "quantities": [], "schedules": [] } msg["lines"].append(current_line) current_schedule = None elif tag == "PIA": if current_line is not None and len(parts) > 1: pia = parts[1].split(':') current_line["buyerPartNo"] = pia[0] if pia else "" elif tag == "SCC": if current_line is not None: current_schedule = { "statusCode": parts[0].split(':')[0], "startDate": "", "endDate": "", "quantity": 0, "unit": "" } current_line["schedules"].append(current_schedule) elif tag == "QTY": qty_parts = parts[0].split(':') qty_value = qty_parts[1] if len(qty_parts) > 1 else "0" qty_unit = qty_parts[2] if len(qty_parts) > 2 else "" if current_schedule is not None: current_schedule["quantity"] = qty_value current_schedule["unit"] = qty_unit elif current_line is not None: current_line["quantities"].append({ "value": qty_value, "unit": qty_unit }) return msg

上面这段代码是解析器的主骨骼。它在处理DTM时做了上下文判断:只有当前已经有了一个SCC窗口对象时,DTM才能被当作窗口起止日期;否则就作为消息头的文档日期。这样就能避免“消息头的日期被误塞进第一个窗口”的问题。

4.3 JSON schema 与下游对接设计

解析器输出JSON后,下游拿到的应该是一个结构清晰的对象。拿前面的示例报文跑一遍,最终JSON大致长这样:

{ "interchange": { "sender": "BUYER01", "receiver": "SUPPLIER99" }, "message": { "referenceNo": "000001" }, "header": { "businessNo": "FORECAST_REF_001", "documentDate": "20230106", "partnerCode": "123456789", "partnerRole": "BY" }, "lines": [ { "lineNumber": "1", "buyerPartNo": "PARTNO-A", "quantities": [], "schedules": [ { "statusCode": "1", "startDate": "20230109", "endDate": "20230115", "quantity": "5000", "unit": "PCE" }, { "statusCode": "1", "startDate": "20230116", "endDate": "20230122", "quantity": "6000", "unit": "PCE" } ] }, { "lineNumber": "2", "buyerPartNo": "PARTNO-B", "quantities": [], "schedules": [ { "statusCode": "1", "startDate": "20230109", "endDate": "20230122", "quantity": "3000", "unit": "PCE" } ] } ] }

这个JSON结构我特意把“quantities”和“schedules”分开。有的DELFOR版本把QTY放在LIN层级(比如行级总数量),有的放在SCC层级(窗口级数量),分开后下游不管是做汇总还是做明细查询都很方便。

4.4 校验规则和错误告警

解析器不能只做转换,还要做正确性校验。我在项目里加上了一套基础校验规则,每一份报文的解析结果都要经过验证才能写库:

  • UNB里的交换控制号必须和UNZ段的控制号一致。
  • UNH的消息参考号必须与UNT段的参考号一致。
  • LIN行号必须从1开始且连续递增,如果中间缺号,意味着报文可能被截断。
  • 每个SCC窗口里的结束日期必须大于等于开始日期。
  • QTY数量必须为非负数。
  • 零件号如果在本地产品主数据中存在,可以做可选的一致性检查,防止客户料号变更导致错配。

如果上述校验不通过,解析器要输出告警,并且不要单独替换旧数据。DELFOR是滚动报文,一旦错误入库,下游计划模块就会拿到错误的预测数据,纠正成本很高。

5. 实测中遇到的坑:这部分标准文档不会写

5.1 一个 LIN 后面挂多个 SCC/DTM 组,别丢预测窗口

我最早写第一版解析器的时候,犯过一个非常典型的错误:我给每个LIN行建了一个“quantity”字段,后面读到新的QTY就直接覆盖。客户报文里PARTNO-A明明有两个预测窗口,结果我只保留了第二个窗口的6000,第一个窗口的5000被覆盖丢了。

这个问题做一次回放测试就暴露了:把这批历史报文解析后和客户侧导出的计划Excel对比,数量对不上。后来改成“SCC出现即新建窗口对象,QTY永远写入当前窗口对象”之后,数据就完全对上了。反馈回来:处理DELFOR的行项目时,一定不要把它想象成“一个产品一行数据”,而要想象成“一个产品下多个时间段的多行数据”,这样才能构建正确的数据模型。

还有一个相关的小陷阱:如果SCC后面明明跟着DTM,但在某些报文中DTM是放在SCC前面的,或者中间穿插了别的段(如GIS等),状态机也要能容忍这些中间段,跳过不认识的段继续解析,而不是直接报错终止。

5.2 日期格式与数量单位,客户“方言”很多

理论上EDIFACT规定了DTM后面的2379要素是日期格式限定符,102表示YYYYMMDD,101表示YYMMDD。但实际报文里,我遇到过某客户把日期写成8位数字但没带限定符,也遇到过同一个文件里同时出现两种格式的情况。如果代码里硬编码“取第2380位当YYYYMMDD”,就会出错。

建议做法是:以2379限定符为准做格式判别;如果限定符缺失,则根据字符串长度做二次判断:8位按YYYYMMDD,6位按YYMMDD。另外,日期入库前统一转成数据库标准日期类型,不要保存成字符串,否则后面做窗口筛选时会出现各种排序问题。

数量单位同样有“方言”问题。PCE、EA、PC其实都表示“个”,但如果下游系统分属不同部门,有的按EA存,有的按PCE存,最后对账时一定会乱。解决方案是在解析层统一单位,配置一张单位映射表,把EA、PC、PCS都归一到PCE,把KG归一到KGM。不要把单位转换留给下游,下游并不了解EDI报文的原始语义。

5.3 回放测试:上线前一定要做的 Diff

最后这部分是我最想强调的。很多项目对EDI解析的测试只做“一条样例报文能跑通”,这远远不够。DELFOR是持续滚动的报文,每周的窗口都会顺延,客户偶尔还会调整字段结构。所以我每次上线前都会做一次“历史回放测试”:把过去至少三个月的真实报文全部重新解析一遍,把运行结果和旧系统已经验证过的数据做Diff。

Diff的重点放在这几个地方:

  • 行项目总数和产品编码集合是否一致。
  • 每个产品的每个窗口的起止日期和数量是否一致。
  • 消息参考号、文档日期、参与方编码是否一致。

如果Diff有差异,不要直接改代码,先查清楚是报文本身格式差异还是解析器逻辑差异。有次我发现所有报文的数量都按1:10放大了,排查到最后发现是某个产品在客户侧的计量单位是“盒”,每盒10个,而我们的主数据里单位是“个”。这不是解析器的问题,而是业务单位换算规则没有落地,这种事只有结合回放测试才能暴露出来。

5.4 对接前一定去要“三样东西”

如果你也要接某个客户的DELFOR,我的建议是:正式开发前先找客户的EDI支持要三样东西,缺一样都别急着写代码。

第一是EDI Mapping Sheet,即字段映射表。它规定了客户用的限定符、段组结构、必填字段和业务含义说明。不要试图从网上的标准文档推断客户的真实结构,每个大客户的兑接规范都有区别,拿到Mapping Sheet之后再写解析器,目标会清晰很多。

第二是真实样本报文,而且至少要覆盖三个不同产品号、不同预测窗口数的样本。很多项目只拿一个文件开发,结果第二个文件一来就崩。样本里的LIN行数、QTY次数、SCC窗口数都必须明显不同,才能验证状态机的健壮性。

第三是业务字段变更记录或产品主数据列表。有些客户会在新财年调整产品编码规则,或者在报文里增加新的限定符。这些变化如果不提前了解,解析器上线后大概率会出现“未知段”或“未知限定符”的告警。

做EDI解析越久,越觉得这活儿的核心其实不是“解析”,而是“语义还原”。把一个单引号结尾的EDIFACT段变成JSON对象,只是数字层面的搬运;把“哪一行的哪个数量属于哪个时间窗口、这个窗口是确认计划还是参考计划、这个数量用什么单位”这些业务语义准确还原出来,才是真正的价值。后面我还在做的一件事是把解析结果做成可视化的报表,让计划员能看到每个产品未来12周的预测趋势,相当于把EDI报文“翻译”成业务人员能直接看懂的语言。这套逻辑一旦跑通,整个供应链计划模块的自动化程度就会明显上一个台阶。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询