電子物流中的EDI 應(yīng)用

電子物流中的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 呢?很簡單灸叼,分三步:

  1. 準(zhǔn)備需要傳輸?shù)臄?shù)據(jù)
  2. 把數(shù)據(jù)轉(zhuǎn)換成EDI 格式
  3. 關(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ù)字格式□锞龋總之解析是個可以不斷豐富的過程唯笙,但我這沒有做很多。

補充閱讀:

  1. 維基百科 - EDI
  2. 中國的港口大全
  3. 實時的港口監(jiān)測信息
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末落剪,一起剝皮案震驚了整個濱河市睁本,隨后出現(xiàn)的幾起案子尿庐,更是在濱河造成了極大的恐慌忠怖,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抄瑟,死亡現(xiàn)場離奇詭異凡泣,居然都是意外死亡,警方通過查閱死者的電腦和手機皮假,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門鞋拟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人惹资,你說我怎么就攤上這事贺纲。” “怎么了褪测?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵猴誊,是天一觀的道長潦刃。 經(jīng)常有香客問我,道長懈叹,這世上最難降的妖魔是什么乖杠? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮澄成,結(jié)果婚禮上胧洒,老公的妹妹穿的比我還像新娘。我一直安慰自己墨状,他們只是感情好卫漫,可當(dāng)我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著肾砂,像睡著了一般汛兜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上通今,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天粥谬,我揣著相機與錄音,去河邊找鬼辫塌。 笑死漏策,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的臼氨。 我是一名探鬼主播掺喻,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼储矩!你這毒婦竟也來了感耙?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤持隧,失蹤者是張志新(化名)和其女友劉穎即硼,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體屡拨,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡只酥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了呀狼。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片裂允。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖哥艇,靈堂內(nèi)的尸體忽然破棺而出绝编,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布十饥,位于F島的核電站怎棱,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏绷跑。R本人自食惡果不足惜拳恋,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望砸捏。 院中可真熱鬧谬运,春花似錦、人聲如沸垦藏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽掂骏。三九已至轰驳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間弟灼,已是汗流浹背级解。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留田绑,地道東北人勤哗。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像掩驱,于是被迫代替她去往敵國和親芒划。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容