電子物流中的EDI 應(yīng)用
背景
EDI 全稱是Electronic data interchange, 即電子數(shù)據(jù)交換超陆。在傳統(tǒng)企業(yè)里,很多流程上的操作或者通信一般是由紙質(zhì)媒介完成的浦马,比如說采購訂單时呀、發(fā)票、訂單同步之類的晶默。但由于紙質(zhì)媒介一切傳播全靠人手谨娜,就會帶來很多不可避免的缺點,比如說操作及同步信息慢磺陡、人力及物力資源消耗大等等趴梢。EDI 的出現(xiàn)就是為了解決紙質(zhì)交互帶來的缺點。它可以極大地提高業(yè)務(wù)效率币他、更快地同步各種狀態(tài)及信息坞靶、減少糾錯流程、接近實時地訪問信息蝴悉,最重要的是它可以省錢彰阴。有研究表明平均來說使用EDI 的成本相比紙質(zhì)交換來說只需要紙質(zhì)的三分之一開銷。而且可以節(jié)省更多的時間(有研究是節(jié)省61%)拍冠,縮短訂單的周期尿这,將流程自動化等等。
估計能看到這文章的人多為互聯(lián)網(wǎng)從業(yè)者庆杜,可能很難想象在這個“信息化”的時代里還有那么多沒有被“信息化”的地方射众。但神奇的是,在完全沒有信息化的情況下欣福,人們也靠人力實現(xiàn)了一切甚至直到這一刻還有那么多的傳統(tǒng)行業(yè)在過著完全不一樣的日常流程责球。
當(dāng)你看到這第一段的時候,你可能會想:這不就是 “接口(API)” 做的事嗎拓劝?EDI 成立之初其實做的就是類似我們現(xiàn)在用API 做的事雏逾。只是年代不同。EDI 最早在1960 年代就出現(xiàn)了郑临。而我們更為熟悉一些的像XML 是1996 年才出現(xiàn)的栖博,而JSON 是2013 年才有。現(xiàn)代互聯(lián)網(wǎng)的歷史還真就這么短厢洞。
話說回來仇让,EDI 與API 的不同之處典奉,在我的理解里,相比我們現(xiàn)在的API, EDI 的概念其實更像是通用的API. 我們API 的“標(biāo)準(zhǔn)” 更多的是大家用相同的技術(shù)丧叽,但各家有各家的定義卫玖。而EDI 是大家用相同的技術(shù),并且大家用相同的結(jié)構(gòu)踊淳。因為EDI 文件中并不包含解釋假瞬。所以每一份EDI 里面用的字段,表達的內(nèi)容都是一樣的迂尝。你也可以理解為是一個沒有包含字段名只有各個字段值的API.
一開始EDI 起源于軍事物流脱茉。但隨著時間的發(fā)展,各行各業(yè)慢慢都用上了這個東西垄开,但又因為不同行業(yè)的需求不同琴许,所以現(xiàn)今EDI 有多個流通的標(biāo)準(zhǔn)。比如說船舶艙單的用的EDI 856溉躲,發(fā)票用的EDI 810榜田,美國最多使用的ANSI X12,全球其他地方使用比較多的UN/EDIFACT 等等签财。不同的行業(yè)串慰、不同的情景都會有一個對應(yīng)的標(biāo)準(zhǔn)可以用偏塞。
原理
那么說了那么多唱蒸,如何使用EDI 呢?很簡單灸叼,分三步:
- 準(zhǔn)備需要傳輸?shù)臄?shù)據(jù)
- 把數(shù)據(jù)轉(zhuǎn)換成EDI 格式
- 關(guān)上冰箱門(bushi
EDI 根據(jù)使用場景的不同神汹,一般EDI 是一個文本文件。里面按照某種標(biāo)準(zhǔn)將對應(yīng)的信息轉(zhuǎn)化成EDI 格式保存古今。至于傳輸它屁魏,你可以用FTP/SFTP/FTPS, AS1/AS2/AS4, OFTP/OFTP2 甚至是用電子郵件發(fā)送這個EDI 文件都行。只要是能從一個地方傳輸一個文本文件到另外一個地方的都行捉腥。甚至有人是做成了API 的也有(格式用的EDI)氓拼。總之大部分情況下EDI 都是直接的端對端傳輸抵碟。但也有少量的VAN(增值網(wǎng)絡(luò))桃漾。
另外一端取到文件后,如果是自動化的可以用程序進行下一步流程拟逮。如果是要給人看的則可以用EDI 翻譯噐或者用解析器去打開它們撬统。
比如說我之前做的一個項目用的EDI 315,315主要用在船運中跟蹤物流信息狀態(tài)及運輸/集裝箱的一些事件詳情等敦迄。它的內(nèi)容如下:
ISA*00* *00* *ZZ*OECGROUP *ZZ*AAA *201120*1304*U*00401*000259937*0*P*>
GS*QO*OECGROUP*AAA*20201120*1304*259937*X*004010
ST*315*0001
B4***I*20201030*0000*CNYTN*DFSU*773057*L*4500*CNYTN*UN*6
N9*BM*OERT210702J01222
N9*BN*MEDUZ7825111
N9*EQ*DFSU7730576
N9*SN*FJ10356721
N9*SCA*OERT
Q2*NONE********045W***L*MAERSK ALGOL
R4*5*UN*CNYTN*YANTIAN PT*CN
DTM*140*20201030*0000*LT
R4*R*UN*CNYTN*YANTIAN PT*CN
DTM*140*20201030*0000*LT
R4*L*UN*CNYTN*YANTIAN PT*CN
DTM*140*20201110*1040*LT
R4*D*UN*USSAV*SAVANNAH*US
DTM*139*20201213*0000*LT
R4*E*UN*USSAV*SAVANNAH*US
DTM*139*20201215*1500*LT
SE*19*0001
GE*1*259937
IEA*1*000259937
這就是一個完整的EDI 文件恋追∑炯#看著可能覺得像亂碼,但其實只是它沒有字段名解釋而已苦囱。
所有的EDI 文件都是由三個塊組成的:
- Element:元素嗅绸,一行里面的內(nèi)容就是一個個不同的元素。
- Segments:段撕彤,段可以理解為一些同類型的元素朽砰,類似于組的概念。對元素進行分類喉刘。像上面的例子中一行就是一個組瞧柔。
- Transaction Sets:事務(wù)集,也可以叫EDI 信息或者EDI 事務(wù)睦裳。當(dāng)信息以段的形式收集好后它們就會組成集造锅。
之所以要規(guī)定這些標(biāo)準(zhǔn),就是要用一個經(jīng)由雙方認(rèn)可的標(biāo)準(zhǔn)去傳輸信息可以用更少量的內(nèi)容去表達更多的事廉邑。
下面我只解釋上面例子中的一段哥蔚,大概了解一下意思知道原理就行。更多詳細(xì)的各段的代表的意思可以看這個文檔了解:315 Status Details (Ocean)
上面這個文檔只是我在網(wǎng)上找的蛛蒙,用的是同個標(biāo)準(zhǔn)但細(xì)節(jié)可能會和我這個例子不完全一樣糙箍,因為同個標(biāo)準(zhǔn)下,不同的公司可能用到的字段不完全相同牵祟,比如有些字段它們公司不需要可能就給省略了深夯。但意思是一個意思。
R4*5*UN*CNYTN*YANTIAN PT*CN
"R4" - 是這節(jié)的頭诺苹,標(biāo)識了這是一行描述 "Port or Terminal" 的相關(guān)內(nèi)容咕晋。
""* - 星號在這份文件中就是個分隔符,沒有實際意思收奔。所以這段本質(zhì)上是:[R4, 5, UN, CNYTN, YANTIAN PT, CN] 這么幾節(jié)信息掌呜。
"5" - 這也是一個約定的值,當(dāng)它是 "5" 時坪哄,代表這在描述 "Active Location".
"UN" - 代表下個值是采用的UNLOCODE, 即港口碼頭代碼表质蕉,描述全球各個港口用的一個表。
"CNYTN" - 這是UNLOCODE 的當(dāng)前這個港口的代號翩肌∧0担可以在這里查詢。即深圳的鹽田港摧阅。
"YANTIAN PT" - 這個是港口的名字汰蓉。PT是Port, 即港口。
"CN" - 這是兩位的ISO 國家代碼棒卷,指中國????
在有說明的情況下其實挺好理解的顾孽。但是沒有說明就會是天書祝钢。。
實施
關(guān)于這節(jié)其實我一直在想需不需要寫若厚。因為在原理清楚了之后其實就沒有很大必要寫了拦英,是個開發(fā)都能整個解析出來的。而且Github 上也挺多現(xiàn)成的類庫的测秸,不過因為標(biāo)準(zhǔn)太多了很大機率你還是得自已實現(xiàn)一個自已需要的疤估。
關(guān)于傳輸?shù)模绻窍耠娮余]件的可以直接用郵件服務(wù)攔截附件并發(fā)送內(nèi)容到解析程序霎冯。如果是ftp 之類的文件服務(wù)可能得設(shè)立一個文件變動的監(jiān)控程序或者是弄個定時器定時掃描铃拇。
關(guān)于解析內(nèi)容的,下面我附個最簡單的解析代碼解析上面這個例子吧沈撞。解析這種東西看使用場景需要可以變得很復(fù)雜也可以很簡單慷荔。一切跟著需求走。
const UNUSED = undefined;
const InterchangeControlHeader = [
'AuthorizationInformationQualifier',
'AuthorizationInformation',
'SecurityInformationQualifier',
'SecurityInformation',
'InterchangeSenderIDQualifier',
'InterchangeSenderID',
'InterchangeReceiverIDQualifier',
'InterchangeReceiverID',
'InterchangeDate',
'InterchangeTime',
'InterchangeControlStandardsIdentifier',
'InterchangeControlVersionNumber',
'InterchangeControlNumber',
'AcknowledgmentRequested',
'UsageIndicator',
'ComponentElementSeparator',
];
const FunctionalGroupHeader = [
'FunctionalIdentifierCode',
'ApplicationSendersCode',
'ApplicationReceiversCode',
'Date',
'Time',
'GroupControlNumber',
'ResponsibleAgencyCode',
'VersionReleaseIndustryIdentifierCode',
];
const TransactionSetHeader = [
'TransactionSetIdentifierCode',
'TransactionSetControlNumber',
];
const BeginningSegmentForInquiryOrReply = [
UNUSED,
UNUSED,
'ShipmentStatusCode',
'Date',
'StatusTime',
'StatusLocation',
'EquipmentInitial',
'EquipmentNumber',
'EquipmentStatusCode',
'EquipmentType',
'LocationIdentifier',
'LocationQualifier',
'EquipmentNumberCheckDigit',
];
const ReferenceIdentification = [
'ReferenceIdentificationQualifier',
'ReferenceIdentification',
];
const StatusDetailsOcean = [
'VesselCode',
UNUSED,
UNUSED,
UNUSED,
UNUSED,
UNUSED,
UNUSED,
'CountryCode',
'VoyageNumber',
UNUSED,
UNUSED,
'VesselCodeQualifier',
'VesselName',
];
const PortOrTerminal = [
'PortOrTerminalFunctionCode',
'LocationQualifier',
'LocationIdentifier',
'PortName',
'CountryCode',
];
const DateTimeReference = ['DateTimeQualifier', 'Date', 'Time', 'TimeCode'];
const TransactionSetTrailer = [
'NumberOfIncludedSegments',
'TransactionSetControlNumber',
];
const FunctionalGroupTrailer = [
'NumberOfTransactionSetsIncluded',
'GroupControlNumber',
];
const InterchangeControlTrailer = [
'NumberOfIncludedFunctionalGroups',
'InterchangeControlNumber',
];
const segments = {
ISA: 'InterchangeControlHeader',
GS: 'FunctionalGroupHeader',
ST: 'TransactionSetHeader',
B4: 'BeginningSegmentForInquiryOrReply',
N9: 'ReferenceIdentification',
Q2: 'StatusDetailsOcean',
R4: 'PortOrTerminal',
DTM: 'DateTimeReference',
SE: 'TransactionSetTrailer',
GE: 'FunctionalGroupTrailer',
IEA: 'InterchangeControlTrailer',
};
const segmentFields = {
[segments.ISA]: InterchangeControlHeader,
[segments.GS]: FunctionalGroupHeader,
[segments.ST]: TransactionSetHeader,
[segments.B4]: BeginningSegmentForInquiryOrReply,
[segments.N9]: ReferenceIdentification,
[segments.Q2]: StatusDetailsOcean,
[segments.R4]: PortOrTerminal,
[segments.DTM]: DateTimeReference,
[segments.SE]: TransactionSetTrailer,
[segments.GE]: FunctionalGroupTrailer,
[segments.IEA]: InterchangeControlTrailer,
};
type Edi315 = {
[key in keyof typeof segments]?:
| Record<Partial<keyof typeof segmentFields>, string>
| Record<Partial<keyof typeof segmentFields>, string>[];
};
const parse = function(
data: string | string[],
segmentSeparator = '\n',
valueSeparator = '*',
): Edi315 {
const result = {};
const availableSegments = Object.keys(segments);
const _data = Array.isArray(data) ? data : data.split(segmentSeparator);
_data.map((line, index) => {
if (!line.replace(valueSeparator, '').trim()) return;
const lineData = line.split(valueSeparator);
const segmentName = lineData[0];
if (!availableSegments.includes(segmentName)) {
console.error('Unknown segment:', line);
return;
}
lineData.slice(1).map((item, idx, array) => {
const fieldName = segmentFields[segments[segmentName]][idx];
if (result[segmentName] === undefined) {
if (['N9', 'R4', 'DTM'].includes(segmentName)) {
result[segmentName] = [];
} else {
result[segmentName] = {};
}
}
if (segmentFields[segments[segmentName]].length != array.length) {
if (idx < 1) {
console.error('Mismatch segment length:', line);
}
return;
}
if (fieldName !== UNUSED) {
if (Array.isArray(result[segmentName])) {
if (result[segmentName][index] === undefined) {
result[segmentName][index] = {};
}
result[segmentName][index][fieldName] = item;
} else {
result[segmentName][fieldName] = item;
}
}
});
});
Object.keys(segments).map(segmentName => {
if (Array.isArray(result[segmentName])) {
result[segmentName] = result[segmentName].filter(
x => x as Record<string, string>,
);
}
});
return result;
};
export default parse;
這樣當(dāng)傳入上面這個例子時你就可以得到如下結(jié)果:
{
ISA: {
AuthorizationInformationQualifier: '00',
AuthorizationInformation: ' ',
SecurityInformationQualifier: '00',
SecurityInformation: ' ',
InterchangeSenderIDQualifier: 'ZZ',
InterchangeSenderID: 'OECGROUP ',
InterchangeReceiverIDQualifier: 'ZZ',
InterchangeReceiverID: 'AAA ',
InterchangeDate: '201120',
InterchangeTime: '1304',
InterchangeControlStandardsIdentifier: 'U',
InterchangeControlVersionNumber: '00401',
InterchangeControlNumber: '000259937',
AcknowledgmentRequested: '0',
UsageIndicator: 'P',
ComponentElementSeparator: '>'
},
GS: {
FunctionalIdentifierCode: 'QO',
ApplicationSendersCode: 'OECGROUP',
ApplicationReceiversCode: 'AAA',
Date: '20201120',
Time: '1304',
GroupControlNumber: '259937',
ResponsibleAgencyCode: 'X',
VersionReleaseIndustryIdentifierCode: '004010'
},
ST: {
TransactionSetIdentifierCode: '315',
TransactionSetControlNumber: '0001'
},
B4: {
ShipmentStatusCode: 'I',
Date: '20201030',
StatusTime: '0000',
StatusLocation: 'CNYTN',
EquipmentInitial: 'DFSU',
EquipmentNumber: '773057',
EquipmentStatusCode: 'L',
EquipmentType: '4500',
LocationIdentifier: 'CNYTN',
LocationQualifier: 'UN',
EquipmentNumberCheckDigit: '6'
},
N9: [
{
ReferenceIdentificationQualifier: 'BM',
ReferenceIdentification: 'OERT210702J01222'
},
{
ReferenceIdentificationQualifier: 'BN',
ReferenceIdentification: 'MEDUZ7825111'
},
{
ReferenceIdentificationQualifier: 'EQ',
ReferenceIdentification: 'DFSU7730576'
},
{
ReferenceIdentificationQualifier: 'SN',
ReferenceIdentification: 'FJ10356721'
},
{
ReferenceIdentificationQualifier: 'SCA',
ReferenceIdentification: 'OERT'
}
],
Q2: {
VesselCode: 'NONE',
CountryCode: '',
VoyageNumber: '045W',
VesselCodeQualifier: 'L',
VesselName: 'MAERSK ALGOL'
},
R4: [
{
PortOrTerminalFunctionCode: '5',
LocationQualifier: 'UN',
LocationIdentifier: 'CNYTN',
PortName: 'YANTIAN PT',
CountryCode: 'CN'
},
{
PortOrTerminalFunctionCode: 'R',
LocationQualifier: 'UN',
LocationIdentifier: 'CNYTN',
PortName: 'YANTIAN PT',
CountryCode: 'CN'
},
{
PortOrTerminalFunctionCode: 'L',
LocationQualifier: 'UN',
LocationIdentifier: 'CNYTN',
PortName: 'YANTIAN PT',
CountryCode: 'CN'
},
{
PortOrTerminalFunctionCode: 'D',
LocationQualifier: 'UN',
LocationIdentifier: 'USSAV',
PortName: 'SAVANNAH',
CountryCode: 'US'
},
{
PortOrTerminalFunctionCode: 'E',
LocationQualifier: 'UN',
LocationIdentifier: 'USSAV',
PortName: 'SAVANNAH',
CountryCode: 'US'
}
],
DTM: [
{
DateTimeQualifier: '140',
Date: '20201030',
Time: '0000',
TimeCode: 'LT'
},
{
DateTimeQualifier: '140',
Date: '20201030',
Time: '0000',
TimeCode: 'LT'
},
{
DateTimeQualifier: '140',
Date: '20201110',
Time: '1040',
TimeCode: 'LT'
},
{
DateTimeQualifier: '139',
Date: '20201213',
Time: '0000',
TimeCode: 'LT'
},
{
DateTimeQualifier: '139',
Date: '20201215',
Time: '1500',
TimeCode: 'LT'
}
],
SE: {
NumberOfIncludedSegments: '19',
TransactionSetControlNumber: '0001'
},
GE: {
NumberOfTransactionSetsIncluded: '1',
GroupControlNumber: '259937'
},
IEA: {
NumberOfIncludedFunctionalGroups: '1',
InterchangeControlNumber: '000259937'
}
}
在這個例子中我并沒有對縮寫類的詞匯或者字段進行拓展缠俺,保留了它們在文件中的樣子显晶,實際上使用的話你可以把它們拓展成人眼可直接閱讀的原意可能會更好點,再有就是對不同的類型的字段進行類型轉(zhuǎn)換也是不錯的壹士,比如說日期的轉(zhuǎn)成日期格式磷雇,數(shù)字的轉(zhuǎn)成數(shù)字格式□锞龋總之解析是個可以不斷豐富的過程唯笙,但我這沒有做很多。
補充閱讀: