Нередко бизнес-процесс компании включает в себя необходимость обработки документации, содержащей штрих-коды, с последующим занесением их в некую учетную систему. Это актуально, например, для сферы ЖД-перевозок и авиакомпаний: в штрих-код купона, присылаемого на электронную почту агентами компании, зашиты номера билетов. Операционисты вынуждены ежедневно обрабатывать и заносить во внутреннюю учетную систему тысячи таких купонов.
Работа рутинная, человеческий фактор провоцирует ошибки. Как автоматизировать процесс и избавить оператора от необходимости ручной обработки писем и их вложений? Мы нашли простое решение с использованием MS SharePoint. По своему обыкновению, мы постарались по максимуму задействовать имеющийся функционал систем, существующую библиотеку и немного своей программистской магии:)
Настройка Exchange
Если вы прочитали документацию и все настроили правильно, то дальше для вас все делается в несколько нажатий, в настройках библиотеки документов:
На сервере с SharePoint настраивается почтовый сервер на прием писем. Входящие письма в виде eml-файлов появляются в файловой системе в папке inbox. SharePoint каждые две минуты проверяет папку inbox, и если там есть файлы, он их обрабатывает и удаляет.
Стоит заметить, что все подряд файлы (с неправильным адресом получателя, с неправильными дополнительными тегами либо созданные с помощью программы Outlook Express) обрабатываться не будут — файл должен быть создан именно сервером SMTP из состава IIS.
В результате, все письма, приходящие на этот адрес, попадают в библиотеку документов:
В каждом письме присутствует одно или несколько вложений в форматах jpg, png, tiff, pdf, в которых может быть один или несколько документов, отмеченных штрих-кодом:
Для распознавания штрих-кодов мы использовали open source библиотеку zxing, с помощью которой можно сканировать и обрабатывать различные типы штрих-кодов. Здесь нам пришлось немного доработать возможности библиотеки, а точнее — алгоритм терминирующего символа, т.к. у заказчика используется своя специфическая кодировка, которая в zxing не поддерживается (хоть и похожа на codabar).
Далее мы реализовали Event Handler, с помощью zxing каждые 15 минут определяющий документы во вложениях писем, которые автоматически попадают на портал через Exchange. Далее он забирает их через библиотеку, вытаскивает вложения, адрес отправителя и код отправителя (личный код агента компании).
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using ADODB;
using CDO;
using ETR.REBT.BarcodeReader;
using Stream = System.IO.Stream;
namespace ETR.BusinessLogic.EBTBlanks
{
internal class EBTMailParser
{
private static readonly string[] SupportedExtensions =
{
"application/pdf"
, "image/jpeg"
, "image/bmp"
, "image/png"
, "image/tiff"
};
private static readonly Regex AgencyCodePattern = new Regex(@"{w*}");
public static EBTMailParseResult ParseEmail(Stream emlStream)
{
var result = new EBTMailParseResult
{
Success = true
};
Message msg = new MessageClass();
ADODB.Stream stream = new StreamClass();
try
{
CopyStream(emlStream, stream);
msg.DataSource.OpenObject(stream, "_Stream");
result.FromAddress = msg.From;
result.SendDate = msg.SentOn;
result.ReceivedDate = msg.ReceivedTime;
string agentCode;
if (TryParseAgencyCode(msg.Subject, out agentCode))
{
result.AgencyCode = agentCode;
}
else
{
result.Status = "Не удалось распознать код Агента. Код должен содержаться в теме письма, содержать только цифры и располагаться внутри фигурных скобок. Например, {12345}";
return result;
}
if (msg.Attachments.Count == 0)
{
result.Status = "В письме не обнаружено ни одного вложения";
return result;
}
result.Attachments = ParseAttachments(msg.Attachments).ToList();
return result;
}
catch (Exception ex)
{
//If we get unknown error - we don't mark letter as parsed an try next time
result.Success = false;
result.Status = "Внутренняя ошибка.";
result.ExceptionMessage = ex.Message;
return result;
}
finally
{
stream.Close();
}
}
private static bool TryParseAgencyCode(string subject, out string result)
{
var allMatchResults = AgencyCodePattern.Matches(subject);
if (allMatchResults.Count != 1 || false == allMatchResults[0].Success)
{
result = null;
return false;
}
result = allMatchResults[0].Value.Substring(1, allMatchResults[0].Value.Length - 2);
return true;
}
private static void CopyStream(Stream netStream, ADODB.Stream adoStream)
{
//adoStream.Open(Type.Missing, ADODB.ConnectModeEnum.adModeUnknown, ADODB.StreamOpenOptionsEnum.adOpenStreamUnspecified, String.Empty, String.Empty);
adoStream.Type = StreamTypeEnum.adTypeBinary;
adoStream.Open();
netStream.Position = 0;
var buffer = new byte[1024];
while (netStream.Read(buffer, 0, buffer.Length) != 0)
{
adoStream.Write(buffer);
}
adoStream.Flush();
}
private static void CopyStream(ADODB.Stream adoStream, Stream netStream)
{
while (!adoStream.EOS)
{
var bytes = (byte[])adoStream.Read(1024);
netStream.Write(bytes, 0, bytes.Length);
}
netStream.Flush();
}
private static IEnumerable<EBTAttachmentParseResult> ParseAttachments(IBodyParts attachments)
{
var barcodeReader = new BarcodeReader(true);
for (var i = 1; i <= attachments.Count; i++)
{
var attachment = attachments[i];
var fileResult = new EBTAttachmentParseResult
{
FileName = attachment.FileName
};
if (false == SupportedExtensions.Any(ct => ct == attachment.ContentMediaType))
{
fileResult.Status = String.Format("Файлы {0} не поддерживаются", attachment.ContentMediaType);
yield return fileResult;
}
var stream = attachment.GetDecodedContentStream();
try
{
var memoryStream = new MemoryStream();
CopyStream(stream, memoryStream);
memoryStream.Position = 0;
var parseResult = barcodeReader.Decode(memoryStream, attachment.FileName);
fileResult.Status = parseResult.AllPages > parseResult.RecognizedPages
? String.Format("Распознано {0} из {1} страниц", parseResult.RecognizedPages,
parseResult.AllPages)
: fileResult.Status;
fileResult.Stream = memoryStream;
if (parseResult.ResultList != null && parseResult.ResultList.Count > 0)
{
fileResult.BarcodeNumbers = parseResult.ResultList.Select(b => ParseBarcode(b.Text)).ToList();
}
}
catch (Exception ex)
{
fileResult.Status = "Внутренняя ошибка.";
fileResult.ExceptionMessage = ex.Message;
}
finally
{
stream.Close();
}
yield return fileResult;
}
}
private static EBTBarcodeParseResult ParseBarcode(string barCode)
{
if (barCode.Length != 15)
return new EBTBarcodeParseResult
{
BarcodeNumber = barCode,
Status = "Не удалось распознать код документа"
};
return new EBTBarcodeParseResult
{
BarcodeNumber = barCode,
CouponNumber = barCode.Substring(1, 13).Insert(3, " ")
};
}
}
}
Если хэндлер находит какую-то ошибку (неправильное расширение файлов, не распознается код агента и т.д.), в EventBus регистрируется событие с ее описанием.
Если ошибок нет, вложения разбираются через библиотеку, из них «вытаскиваются» штрих-коды, с которых считывается информация. На каждый определенный штрих-код создается бланк в SharePoint, который уже и обрабатывают операционисты, когда получают оригиналы документов.
В завершение, в EventBus отправляется событие с сообщением о том, что письмо разобрано, информацией сколько файлов в нем найдено, сколько купонов во вложении, сколько билетов в купонах. Все заинтересованные лица получают push-уведомление.
Данный бизнес-процесс — маленькая часть большой работы офиса крупной транспортной компании. Также его легко можно транслировать и на другие сферы, где необходимо много и рутинно обрабатывать входящую документацию, и где используются SharePoint и Exchange.
Наш постулат в таких условиях — не переставать совершенствование существующей системы, автоматизировать все рутины, используя доступные средства, смекалку и свои навыки в программировании.
Автор: eastbanctech