独自のERPシステムを作成したとき、ver。 2.0

私の前の記事は、読者からの多くの質問とコメントを引き起こしました。 確かに、それはかなりくしゃくしゃになり、すべてを説明していませんでした。 コメントで受け取った質問から説明して、このケースをもう一度述べたいと思います。



だから、与えられた:宅配注文に特化した大規模なレストランのビジネスネットワークの支店。



市内に散らばったいくつかのキッチン。 各キッチンには、注文、料理人、完成品の包装、配達員の受け手がいます。 クライアントは、サイト、モバイルアプリケーションを介して注文を破棄するか、注文受信者を呼び出します。 理想的には、1時間以内に注文を確認した後、この注文はクライアントと行う必要があります。 すなわち 準備と梱包の合計時間は20分、配達時間は40分です(これは、人口100万人の都市で、対応する距離と渋滞が発生していると言わなければなりません)。 注文の実際の構成に関する情報に加えて、クライアントは支払方法(現金/カード、配達が必要かどうか、金額)と配達先住所を通知します。



既存のシステムの主な問題は、次のように定式化できます。



  1. 各段階で従業員を適切に管理できない

    • 料理人を除いて、誰もキッチンの現在の負荷を知りません。宅配便を除いて、彼らの現在の負荷です。
    • 遅延は修正されていません(宅配便業者が15分以上遅れている場合-これは会社全体にとって大きな相殺ではありません)。
    • どの日に何人の料理人と宅配便が必要かを予測することは困難です。
    • 注文が大きい場合は、宅配便業者が常に2番目のパッケージを忘れてしまい、キッチン/パッケージングなどに頼ってしまいます。
  2. 配達先住所のエラー。宅配便業者はどこに行くべきかを常に理解しているわけではありません:間違った通りのスペル、物理的に不在の家番号、およびこの情報の正確さの制御不足によるその他の問題。
  3. キッチンによる注文の分配の不足 。 キッチンの負荷はほぼ同じである必要があります。 つまり、各注文は、作業負荷と交通渋滞に基づいたポイントに従って配布される必要があります。 パラグラフ1の制御を実装せずに実装することは不可能です。


もちろん、同時に、キャッシュデスク(現金の返品/消耗品、現金支払い、食料を購入するためのあらゆる種類の技術便など)の完全なサポート、シフト-期間ごとにポイントが責任を負わなければなりません。 そして、おそらくここにリストする価値がないその他のささいなこと。 TKは素晴らしいです。



多くの人が尋ねます:倉庫物流はどうですか? 腐敗した商品、調達の論理などを説明する方法は? 次のステップでこれを実装することにしました。 同様に、消耗品/ 1の到着物をアンロードし、それを通じて給与を転記するなど。 つまり、最初に運用管理を実装し、次に会計処理を行います。



完成した実装をこのビジネスに統合することが不可能だったのはなぜですか? 市場には何もありません。 私と協力し始める前に、顧客は、完成したシステムを必要に応じて完成させるためにかなりの金額を支払う準備ができていました。 しかし、これは見つかりませんでした。

また、顧客の特別な願いは、すべてがスマートに機能することでした。 彼の現在のシステムであるクラウドは、かろうじてi3のブラウザーを投入しています。 そして、負荷がピークのとき、1分あたり平均1件の注文が届くと、数秒のインターフェースの遅れが実際の顧客の損失であり、それに応じてお金が失われます。 しかし、システムには単一のサーバーも必要でした。



それで、私はプロジェクトを始めました。



開発ツールは、Rad Studioによって選択されました。 なんらかの理由でスタジオはあまり人気がありませんが、この場合、まず、すべてのデバイスで単一のコードを取得したかったのです。複数の開発環境で同じデータ構造を何度も登録する余裕がないためです。次に、変更を加えて10か所で編集します。 次に、すべてのプラットフォームでコンパイルされたアプリケーションを取得します。これは理論的には常に迅速に動作するはずです。 しかし何よりもまず、オペレーターの職場のスピードが心配でした。



最初に必要だったのは、既存のシステムとの統合です。 それからの注文は、遅滞なく新しいものに落ちなければなりません。 スニファーを少し使用すると、プログラムが1分間に1回サーバーをポーリングし、新しい注文がある場合はpdfを印刷することがわかりました。 半日後、私のプログラムは元のプロトコルを完全にコピーしました(使用可能なドルフィンのソースコンポーネントのおかげで、生のソケットに煩わされることなくhttpヘッダーを簡単に調整できます)が、プリンターへの印刷に加えて、これらのpdfを解析し、そこから注文を読み取りデータベースにロードします。



このような素晴らしいプログラムpdftotextがあります。 オプションで実行

–enc UTF-8 –raw







そして、必要な情報をすべて取り出すことで問題なく解析できる.txtファイルがあります。

しかし、この方法にはいくつかの制限がありました。 第一に、必要なすべての情報がアップロードされたわけではありませんが、オペレーターはいくつかのフィールド(たとえば、変更)を補充する必要があり、第二に、オーダーによる同期のみでした。 たとえば、配達便をあちこちで形成する必要がありました。 したがって、通過するすべての情報を分析するプロキシが作成されました。プロトコル全体が暗号化されていないJSON上にあり、この問題は解決されました。



2番目の問題は、アドレスベースです。 FIASをダウンロードして、データベースにアップロードする必要があることは明らかです。 しかし、第一に、拠点全体が必要ではなく、アパート、古いレコードなどは必要ありません。実際、興味深いのは住所そのものではなく、それをどこに持って行くかです。 私は集落、通り、家だけを降ろすことにしました。 そして、座標の記録の追加された各フィールドの家のテーブルで。 しかし、控えめに言うと、すべての家がFIASにあるわけではないので、手動で家(および場合に応じて道路)に入力する機能を追加する必要がありましたが、データベースへのその後の更新でこのレコードと一致し、対応するGUIDをバインドできる形式で。

ジオコーディングのために、Yandexを接続しました。 私たちの街で、彼は今最高です。 そのため、まずPDFから住所を通り、家、床、アパートなどに解析し、次にこの家が存在する場合はデータベースを調べ、座標を調べます。 座標がない場合は、極東またはシベリアにいくつかのポイントが表示され、決済がまったくないため、市の中心から約30 kmをチェックしてジオコーディングします。 Yandexはこの不具合を理解していませんでした。



そのようなアドレスがない場合は、以前のアップロードを見てください。 突然、このような「ストリートハウス」の組み合わせがすでに出会ったので、そこから座標を取ります。 つまり、たとえば、通りがエラーで元のデータベースに書き込まれている場合、新しい順序ではなく、1回修正するだけで十分です。

アドレスは、API 1.1を介してYandexマップを使用して表示されました。 私が必要とするすべてのニーズに開発者IDは必要ありません。 確かに、TWebBrowserには重大な制限(Webページを表示するコンポーネント、この場合はマップ)があり、メインプログラムから適切なタイミングで任意のスクリプトを実行できますが、どのような形式でもデータを受信することはできません。 ただし、興味深いハックがあります。データを受け入れる必要がある場合は、スクリプトで実行する必要があります。



 function ApplyPoint(){ var text = 'http://ya.ru/1.htm?&P&&'; text = text + '[' + placemark.getGeoPoint().getY(); text = text + ',' + placemark.getGeoPoint().getX()+']'; window.location = text; };
      
      





そしてDelphiでは:
スクリプトを実行します。



 procedure CMapElement.ApplyEditPoint; begin if not assigned(control) then exit; {$IFDEF CLIENT_BUILD} (control as TTMSFMXWebBrowser).ExecuteJavascript('ApplyPoint();'); {$ENDIF} end;
      
      





それから:



 procedure CMapElement.WebBrowser1DidFinishLoad(ASender: TObject); var s,s2 : string; p : integer; co : ArrMapCoord; begin if not assigned(control) then exit; {$IFDEF CLIENT_BUILD} {$IFDEF MSWINDOWS} s :=(control as TTMSFMXWebBrowser).FWebBrowser.GetRealURL; {$ELSE} assert(false); {$endif} if (pos('ya.ru', s) > 0) and not AlreadyReload then begin p := Pos('?&',s); delete(s,1,p+1); s2 := s; p := Pos('&&',s); delete(s,1,p+1); delete(s2,p,9999); if s2 = 'P' then //   (  ) begin WeEditPoint := false; self.DecodePolyString(s,co); if Length(co) = 1 then begin AddressPoint.la := co[0].la; AddressPoint.lo := co[0].lo; end; AlreadyReload := true; end else if s2 = 'R' then //   begin WeCalcRoute := false; if Assigned(g_CurrentScriptRunner) then begin g_CurrentScriptRunner.OnExternalAction('MapRouteDist',s); end; end else if StrToInteger(s2,p) then begin Polys[p-1].Poly := s; end; AlreadyReload := true; LoadMap; end; MapLoaded := true; {$ENDIF} end;
      
      







つまり、URLでエンコードされたパラメーターを使用してページの読み込みを開始し、コールバック段階でこの瞬間をフィルター処理します。 このハックにより、Delphiでのブラウザの使用が非常に柔軟になります。問題なく地図上でマーカーを移動したり、ルートの長さを確認したり、配信ジオポリゴンを編集したりできます。



まあ、これはすべて何らかの形でリンクする必要があります。 特定の顧客向けのERPシステムだけでなく、作りたかったのです。 構成ファイルから構成され、すべてのロジックとインターフェースが実行可能ファイル自体に結び付けられていないことを十分に普遍的にしたかったのです。 これで、すべての構成が1つのxmlファイルに含まれます。 データベース構造全体と、すべてのインターフェイスとスクリプトを含むこのファイルがサーバーによって読み取られ、必要な部分がクライアントに発行されます。



また、インターフェイスとイベントハンドラスクリプト用のエディタも作成しました。 特定のテーブルのフィールドが変更されると、個別のスクリプトが実行され、この変更の可能性がチェックされるとともに、関連するもの(小教区の生成など)が生成されます。 すべてのコンポーネントは、標準のものとは完全に異なるプロパティを持っているため、独自のものです。 たとえば、表の列は何を表示するかを即座に示し、テキストまたはアイコン(カード支払い/現金など)を表示します。



スクリプトシステムはPascalcを使用して作成されました 。 このライブラリは15年以上前のものですが、非常に成功しています。軽量で、簡単に拡張可能で、あらゆるニーズに合わせて変更可能です。 したがって、モバイルデバイスのPascal標準に従うために、コンパイラディレクティブを含めます

{$ZEROBASEDSTRINGS OFF}







Destroy-> Freeのようないくつかの編集を行い、win apiの使用を削除すると、どのOSでもスクリプトは問題なく動作し、ライブラリは同様のスクリプトを処理します。



スクリプトの例
 CurOrderState := GetNewValueF('Orders','OrderState'); LastOrderState := GetOldValueF('Orders','OrderState'); IsCashbackOnCurier := GetNewValue('Orders','CashbackOnDeliverer') = 'True'; PayType := GetNewValueF('Orders','PayType'); OldPayType := GetOldValueF('Orders','PayType'); OrderTurn := GetNewValueF('Orders','Turn'); CurTurn := GetConstantValue('CurrentTurn'); OrderNo := GetNewValue('Orders','OrderNo'); OrderPrice := GetNewValueF('Orders','Price'); PaySum := GetNewValueF('Orders','PaySum'); LastPaySum := GetOldValueF('Orders','PaySum'); CurTurn := GetConstantValue('CurrentTurn'); CurCashbox := GetConstantValue('CurrentCashbox'); if (OrderTurn > 0) and (OrderTurn <> CurTurn) then begin ShowMessage('      .  №'+IntToStr(OrderNo)); exit; end; if (LastOrderState = 9) then begin ShowMessage('     '); exit; end; if (LastOrderState = 8) then begin ShowMessage('     '); exit; end; if (LastOrderState <> 8) and (CurOrderState = 8) then begin TransactionCanAccept; exit; end; if (CurOrderState > 2) and (CurTurn < 1) then begin ShowMessage('  .     '); exit; end; if (CurOrderState > 4) and (CurCashbox < 1) and (CurOrderState <> 8) then begin ShowMessage('  .     '); exit; end; //  ,     . if IsCashbackOnCurier then begin if PaySum <> LastPaySum then begin ShowMessage('    '); exit; end; Price := GetNewValueF('Orders','Price'); LastPrice := GetOldValueF('Orders','Price'); if Price <> LastPrice then begin ShowMessage('    '); exit; end; end; //   if (CurOrderState >= 6) and (CurOrderState < 8) and (PayType = 1) and (not IsCashbackOnCurier) then begin if (PaySum - OrderPrice) > 0 then begin ss := '    №'+IntToStr(OrderNo); CreateCashboxDoc(ss,1,OrderPrice - PaySum); IsCashbackOnCurier := True; SetValue('Orders','CashbackOnDeliverer',True); end; end; //      if (CurOrderState < 6) and IsCashbackOnCurier then begin if (PaySum - OrderPrice) > 0 then begin CreateCashboxDoc('     №'+IntToStr(OrderNo),1,PaySum - OrderPrice); IsCashbackOnCurier := False; SetValue('Orders','CashbackOnDeliverer',False); end; end; //    if (CurOrderState >= 8) and IsCashbackOnCurier then begin if (PaySum - OrderPrice) > 0 then begin ss := '    №'+IntToStr(OrderNo); CreateCashboxDoc(ss,1,PaySum - OrderPrice); IsCashbackOnCurier := False; SetValue('Orders','CashbackOnDeliverer',False); end; end; //      if (CurOrderState = 9) and (LastOrderState <> 9) and (PayType = 1) then begin ss := '    №'+IntToStr(OrderNo); CreateCashboxDoc(ss,2,OrderPrice); end; //           if (LastOrderState < 3) and (CurOrderState >= 3) then begin if CurTurn <= 0 then begin ShowMessage('  .    .'); exit; end; SetValue('Orders','Turn',CurTurn); CurFilial := GetConstantValue('FilialID'); SetValue('Orders','Filial',CurFilial); end; if (CurOrderState <= 2) and (LastOrderState > 2) then begin SetValue('Orders','Turn',0); end; TransactionCanAccept;
      
      







つまり、シフト、キャッシュデスク、必要に応じて到着/消耗品などのチェックが行われます。 また、このアルゴリズムで何かを変更した場合、プログラムを再構築してすべてを更新する必要はありません。 クライアントがログインするのに十分です。 残念ながら、スクリプト用のAPIを処方するとき、急いでいた。 したがって、たとえば、ここのデータベースのテーブルで指定されている定数は、単なる数値です。 しかし、それはいつでも書き換えることができ、コードはまったく複雑でも大きくもないので、今のところはそのままにしておくことにしました。



次に重要なケースはオペレータープログラムです。 すべてのデータはクラウド内にあるため、遅滞なく受信し、変更を生成する必要があります。 サーバー接続を最大限に活用することにしました。 RAMにキャッシュしていつでもアクセスできる場合、同じレコードを100回ドラッグするのはなぜですか?

すべての関連する注文、顧客、住所などをメモリにアンロードします、つまり、テーブル全体ではなく、バカになり、メモリはすぐに終了しますが、必要なテーブルの部分だけです。 プロセッサのキャッシュのように。 データが含まれている場合はすぐに振り返り、そうでない場合は、サーバーがデータを返すまで待つ必要があります。 テーブルの「変更にサブスクライブする」メカニズムも規定されていました。あるテーブルのレコードが変更された場合、すべてのサブスクライバーはこれについて新しいデータで通知を受け取ります。 その結果、レイテンシに関してアプリケーションを操作する快適さが最大になりました。 フィルターに対する反応は、変更、並べ替え、注文の開始など、すべて瞬時に行われます。



次に、別の宅配便アプリケーションが作成されました。 バックグラウンドでサーバーにトラックを送信します。 つまり、オペレータは、宅配便の動きをリアルタイムで監視できます。 また、注文の配達を確認するには、宅配便業者が配達ポイントから特定の半径内になければなりません。 この時点でインターネットがない場合、プログラムはインターネットが表示されるとすぐに注文が配信されたという情報を送信します。 当然、プログラムからYandexナビゲーターを起動できます。これにより、アドレスへの道が自動的に開かれます。 マップ上のすべての住所を表示するか、2つのボタンを押すなどして顧客に電話をかけることができます。



これらはすべて、ある部門で「戦闘中」にテストされ、獲得されました。 苦情はありませんでした。 より正確には、最初は宅配便アプリケーションにいくつかの問題がありました。インターネット接続が不十分な場合、応答があまりよくありませんでした。 さらにデータをキャッシュし、いくつかの最適化を実行し、すべてが遅延なく機能するようになりました。



しかし、プロジェクトはそれ以上開発されていません。 顧客は次の理由を示しました。



  1. 「コールド」クーリエアプリケーションを4秒で起動します。 長すぎる。
  2. ポイント1のため、プログラミング言語を変更し、すべてを完全に書き換える必要があります。


これで私たちは別れました。



これが物語です。 私は今、配達と予算の穴があるレストラン事業のための不完全なシステムを持っています。 今、私はそれをどうするかを考えます。 それを捨てるのは残念です。 しかし、市場は非常に狭く、閉鎖されています。



All Articles