Рассмотрим пример типовой задачи, которую можно решить на стороне сервера. Пусть существует определенный заказ в статусе “Новый”. Он содержит ряд строк с указанными товарами и их количеством на складе. Покупатель оплачивает заказ, следовательно, товары, входящие в его покупку, должны стать недоступны для повторной покупки. То есть оплаченный товар перестает быть “товаром в магазине” и становится собственностью покупателя. В такой ситуации при переводе заказа в статус “Оплаченный” товар должен списаться со складов.
Решение данной задачи не требует динамического отклика на стороне клиента и, соответственно, может быть реализовано в процессе обработки запроса об изменении статуса заказа на стороне сервера. Следовательно, нам нужно отследить, когда заказ будет находиться в состоянии Update с изменением статуса на Paid. В этом случае нам нужно выполнить поиск товара на складах и списать товар. При этом если определенного товара на складе более не остается, нужно удалить запись об этом товаре:
Разобьем данную логику на конкретные шаги:
- уточнить состояние (заказ может быть новым или измененным) и статус заказа (проверяем, чтобы заказ имел статус “Оплаченный”);
- вычитать и отсортировать все записи о товарах на складах в соответствии со списком товаров в заказе;
- перебрать все товары определенного типа на всех складах и:
- если товара на складах недостаточно - вывести сообщение об ошибке;
- если конкретного товара на очередном складе достаточно, то списать нужное количество товара и обнулить счетчик списания;
- если конкретного товара на очередном складе недостаточно, то удалить запись о текущем товаре на данном складе и уменьшить счетчик списания на то количество товара, которое имеется на данном складе.
Реализуем эту логику в ранее созданном бизнес-сервере OrderBS. В процессе работы используйте брейкпоинты, чтобы контролировать значения переменных.
Условия применения логики
Прежде всего, необходимо реализовать проверку необходимых условий для применения описанной логики (шаг 1). Для этого в секции программиста (между комментариями “Start” и “End”) в теле метода OnUpdateOrder проверим ряд условий:
public virtual ICSSoft.STORMNET.DataObject[] OnUpdateOrder(IIS.Shop.Order UpdatedObject)
{
// *** Start programmer edit section *** (OnUpdateOrder)
if ((UpdatedObject.GetStatus() == ObjectStatus.Created
|| UpdatedObject.GetStatus() == ObjectStatus.Altered)
&& Array.IndexOf(UpdatedObject.GetAlteredPropertyNames(), "Status") >= 0
&& UpdatedObject.Status == OrderStatus.Paid)
{
result = changeOrderStatusPaid(UpdatedObject);
}
return new ICSSoft.STORMNET.DataObject[0];
// *** End programmer edit section *** (OnUpdateOrder)
}
В этом коде мы проверяем через операцию “или” два возможных состояния заказа (1 и 2 строки), а после с использованием операции “и” проверяем, что нам нужна запись, у которой поле “Статус” изменилось (3 строка) и равно на данный момент статусу “Оплачено” (4 строка).
При добавлении данного кода обращение к объекту ObjectStatus будет подчеркнуто красным цветом. Для того, чтобы исправить эту ошибку, нам нужно указать использование соответствующего пространства имен в данном классе. Для исправления подобных ошибок можно навести курсор на “проблемный” объект, раскрыть список предлагаемых исправлений и выбрать подключение требуемого пространства имен:
Если все выполнено верно, то ошибка исчезнет.
Дальнейший код, который мы добавим, может выглядеть достаточно громоздко. Поэтому создадим для действий с Заказом и Товарами на складах отдельный метод changeOrderStatusPaid(), который будет возвращать список измененных объектов в основной метод бизнес-сервера:
public class OrderBS : ICSSoft.STORMNET.Business.BusinessServer
{
// *** Start programmer edit section *** (OrderBS CustomMembers)
/// <summary>
/// Перевод заказа в статус "Оплачено"
/// </summary>
/// <param name="UpdatedObject">Заказ</param>
/// <returns>Список измененных объектов (заказ + товары на складе)</returns>
private static DataObject[] changeOrderStatusPaid(IIS.Shop.Order UpdatedObject)
{
// Определим массив, который будем возвращать для обновления.
DataObject[] ret = new DataObject[0];
return ret;
}
// *** End programmer edit section *** (OrderBS CustomMembers)
// *** Start programmer edit section *** (OnUpdateOrder CustomAttributes)
// *** End programmer edit section *** (OnUpdateOrder CustomAttributes)
public virtual DataObject[] OnUpdateOrder(IIS.Shop.Order UpdatedObject)
{
// *** Start programmer edit section *** (OnUpdateOrder)
if ((UpdatedObject.GetStatus() == ObjectStatus.Created
|| UpdatedObject.GetStatus() == ObjectStatus.Altered)
&& Array.IndexOf(UpdatedObject.GetAlteredPropertyNames(), "Status") >= 0
&& UpdatedObject.Status == OrderStatus.Paid)
{
result = changeOrderStatusPaid(UpdatedObject);
}
return new DataObject[0];
// *** End programmer edit section *** (OnUpdateOrder)
}
}
Прежде всего, для любого метода рекомендуется добавлять комментарии для автодокументации (Documentation comments), в которой минимально указываются:
-
- описание метода; -
- параметр метода с указанием имени и описания;
-
- описание возвращаемых значений (если метод возвращает не void).
Кроме того, так как мы будем в дальнейшем часто использовать тип DataObject, мы упростили обращение к нему, добавив требуемое пространство имен с использованием конструкции using, и тем самым избавились от необходимости писать полное имя типа - ICSSoft.STORMNET.DataObject.
Вызовем добавленный метод (changeOrderStatusPaid) из основного (OnUpdateOrder) и вернуть его результат в качестве списка измененных объектов:
public virtual DataObject[] OnUpdateOrder(IIS.Shop.Order UpdatedObject)
{
// *** Start programmer edit section *** (OnUpdateOrder)
DataObject[] result = new DataObject[0];
if ((UpdatedObject.GetStatus() == ObjectStatus.Created
|| UpdatedObject.GetStatus() == ObjectStatus.Altered)
&& Array.IndexOf(UpdatedObject.GetAlteredPropertyNames(), "Status") >= 0
&& UpdatedObject.Status == OrderStatus.Paid)
{
result = changeOrderStatusPaid(UpdatedObject);
}
return result;
// *** End programmer edit section *** (OnUpdateOrder)
}
Проверим, все ли верно мы выполнили. Для этого запустим сервер в режиме отладки и поставим брейкпоинт в строку с оператором return в методе changeOrderStatusPaid:
В клиентском приложении переведем Заказ 2 в статус “Оплаченный” и сохраним его: на сервере сработает созданный брейкпоинт в новом методе. Продолжим выполнение кода на сервере (кнопка Continue) и обновим страницу в браузере: убедимся, что все изменения сохранились. Далее переведем текущий заказ обратно в статус “Новый” и сохраним его: брейкпоинт в данном случае не сработает. Это означает, что метод работает корректно - метод changeOrderStatusPaid не будет вызываться для новых заказов.
Реализация основной части логики
Теперь, когда для логики перевода Заказа в статус “Оплаченный” создан отдельный метод, можно реализовывать основной функционал. Аналогичная логика реализована в разделе “Работа с бизнес-сервером” (Практическое руководство по веб-разработке на Flexberry ASP.NET).
В числе вносимых в код изменений (по сравнению с кодом, который представлен по ссылке выше) мы добавим возможность сортировки вычитываемых товарных позиций со складов по номеру склада. Для этого нужно обновить класс StoreProduct во Flexberry Designer: необходимо добавить невидимый атрибут Storehouse.Number в представление StoreProductE.
После этого следует обновить проект с объектами данных на бэкенде (выполнить перегенерацию соответствующего проекта). Теперь мы легко получим доступ из объекта конкретной позиции товара на складе к характеристике самого склада, в частности к его номеру. Условимся, что товары будут списываться всегда со складов в порядке увеличения порядкового номера склада.
Реализуем теперь основную логику. Прежде всего, нужно вычитать список всех требуемых товарных позиций со всех складов:
private static DataObject[] changeOrderStatusPaid(IIS.Shop.Order UpdatedObject)
{
// Определим массив, который будем возвращать для обновления.
DataObject[] ret = new DataObject[0];
// Построим ограничение и вычитаем все объекты StoreProduct, которые нам подходят.
Order order = UpdatedObject;
Function lf = null;
for (int i = 0; i < order.OrderItem.Count; i++)
{
Function storeProduct = FunctionBuilder.BuildEquals<StoreProduct>(x => x.Product, order.OrderItem[i].Product);
if (order.OrderItem[i].Product != null)
{
lf = lf == null ? storeProduct : FunctionBuilder.BuildOr(lf, storeProduct);
}
}
LoadingCustomizationStruct lcs = LoadingCustomizationStruct.GetSimpleStruct(typeof(StoreProduct), "StoreProductE");
lcs.LimitFunction = lf;
// Сортировка по номеру склада
var sortColumns = new List<ColumnsSortDef>();
sortColumns.Add(new ColumnsSortDef("Storehouse.Number", SortOrder.Asc));
lcs.ColumnsSort = sortColumns.ToArray();
DataObject[] objs = DataServiceProvider.DataService.LoadObjects(lcs);
return ret; // БРЕЙКПОИНТ
}
Результатом выполнения данного участка кода является вычитанный из БД список товарных позиций на складах в переменной objs, соответствующих списку товаров в заказе. Проверим список объектов данных в этой переменной, установив брейкпоинт на return ret (см. предыдущий скриншот): мы ожидаем увидеть две записи (1 и 2 ед. товара на 1-м и 2-м складах соответственно).
Далее для удобства работы сформируем отсортированные по первичному ключу товаров пары “ключ-значение”, в которые в качестве значений добавим коллекцию вычитанных товарных позиций с соответствующим первичным ключом товара:
private static DataObject[] changeOrderStatusPaid(IIS.Shop.Order UpdatedObject)
{
...
DataObject[] objs = DataServiceProvider.DataService.LoadObjects(lcs);
// Разместим вычитанные объекты в отсортированном списке для удобного доступа в дальнейшем.
SortedList sl = new SortedList();
for (int i = 0; i < objs.Length; i++)
{
var primaryKey = ((StoreProduct)objs[i]).Product.__PrimaryKey;
ArrayList productList;
if (sl.ContainsKey(primaryKey))
{
productList = (ArrayList)sl[primaryKey];
productList.Add(objs[i]);
}
else
{
productList = new ArrayList();
productList.Add(objs[i]);
sl.Add(primaryKey, productList);
}
}
return ret; // БРЕЙКПОИНТ
}
Проверим результат (брейкпоинт прежний).
Теперь мы имеем единую структуру данных, которая хранит информацию о том, что товар с конкретным id найден на двух складах.
Реализуем непосредственно логику списания товаров из оплаченного заказа со складов:
private static DataObject[] changeOrderStatusPaid(IIS.Shop.Order UpdatedObject)
{
// Определим массив, который будем возвращать для обновления.
DataObject[] ret = new DataObject[0];
// Построим ограничение и вычитаем все объекты StoreProduct, которые нам подходят.
Order order = UpdatedObject;
Function lf = null;
for (int i = 0; i < order.OrderItem.Count; i++)
{
Function storeProduct = FunctionBuilder.BuildEquals<StoreProduct>(x => x.Product, order.OrderItem[i].Product);
if (order.OrderItem[i].Product != null)
{
lf = lf == null ? storeProduct : FunctionBuilder.BuildOr(lf, storeProduct);
}
}
LoadingCustomizationStruct lcs = LoadingCustomizationStruct.GetSimpleStruct(typeof(StoreProduct), "StoreProductE");
lcs.LimitFunction = lf;
// Сортировка по номеру склада
var sortColumns = new List<ColumnsSortDef>();
sortColumns.Add(new ColumnsSortDef("Storehouse.Number", SortOrder.Asc));
lcs.ColumnsSort = sortColumns.ToArray();
DataObject[] objs = DataServiceProvider.DataService.LoadObjects(lcs);
// Разместим вычитанные объекты в отсортированном списке для удобного доступа в дальнейшем.
SortedList sl = new SortedList();
for (int i = 0; i < objs.Length; i++)
{
var primaryKey = ((StoreProduct)objs[i]).Product.__PrimaryKey;
ArrayList productList;
if (sl.ContainsKey(primaryKey))
{
productList = (ArrayList)sl[primaryKey];
productList.Add(objs[i]);
}
else
{
productList = new ArrayList();
productList.Add(objs[i]);
sl.Add(primaryKey, productList);
}
}
// Определим строчку для сообщения об ошибке.
string errStr = string.Empty;
ArrayList retObjs = new ArrayList();
// Проверим наличие товара на складах, если не хватает, то выдадим сообщение об ошибке, если хватает, то вычитаем количество.
for (int i = 0; i < order.OrderItem.Count; i++)
{
if (sl.ContainsKey(order.OrderItem[i].Product.__PrimaryKey))
{
ArrayList arl = ((ArrayList)sl[order.OrderItem[i].Product.__PrimaryKey]);
int productTotalCount = 0;
for (int j = 0; j < arl.Count; j++)
{
productTotalCount += ((StoreProduct)arl[j]).Amount;
}
if (productTotalCount < order.OrderItem[i].Amount)
{
errStr += " Не хватает товара \"" + order.OrderItem[i].Product.Name
+ "\" в наличии: " + productTotalCount
+ ", требуется " + order.OrderItem[i].Amount
+ Environment.NewLine;
}
else
{
int count = order.OrderItem[i].Amount;
for (int j = 0; j < arl.Count; j++)
{
if (count > 0 && ((StoreProduct)arl[j]).Amount > count)
{
((StoreProduct)arl[j]).Amount -= count;
count = 0;
retObjs.Add(arl[j]);
}
else if (count > 0)
{
count -= ((StoreProduct)arl[j]).Amount;
((StoreProduct)arl[j]).SetStatus(ObjectStatus.Deleted);
retObjs.Add(arl[j]);
}
}
}
}
else
{
errStr += "Товар \"" +
order.OrderItem[i].Product.Name + "\" в наличии отсутствует." + Environment.NewLine;
}
// В случае, если чего-то не хватило, сообщаем об этом пользователю.
if (errStr != string.Empty)
{
throw new Exception(errStr);
}
// Если всё нормально, то возвращаем массив объектов, которые надо обновить.
ret = new DataObject[retObjs.Count]; retObjs.CopyTo(ret, 0);
}
return ret;
}
Проверим, как работает написанный нами код. Для этого зафиксируем для себя состояние складов до изменения статуса заказа:
Ожидаем, что на первом складе совсем не останется товара “Монитор игровой MSI Optix MAG241CP”, а на втором останется только 1 ед. указанного товара. Для проверки выполним два шага:
-
Выставим Дату оплаты Заказу 2 перед изменением статуса
Important: Если вы не проставите Дату оплаты, то в дальнейшем без нарушения логики или дополнительных действий (например, манипуляцией этим полем в БД) у вас не получится её выставить. -
Переведем Заказ 2 в статус "Оплаченный", сохраним его и посмотрим, какие товары остались на складах:
Код работает корректно: со складов списалось 2 единицы товара “Монитор игровой MSI Optix MAG241CP” в указанной нами последовательности.
Итог
В результате действий, описанных в данной главе, мы реализовали часть серверной логики, которая отрабатывает в момент сохранения. Действия, произведенные на сервере, отличаются высоким уровнем безопасности, так как полностью изолированы от потенциального пользователя. Их скорость также выше, чем у аналогичных манипуляций на стороне клиента. Иногда в бизнес-серверах ember-приложений, сгенерированных при помощи Flexberry Designer, дублируется клиентская логика изменения в моделях. Это касается переопределения операций создания, обновления и удаления записей.
Самостоятельная работа
Вы можете выполнить следующие доработки в приложении самостоятельно для более полной реализации функционала приложения:
- при изменении статуса Заказа на “Оплаченный” создавать на стороне сервера Накладную, к которой был бы уже прикреплен заказ, установлены статус “Новый” и выставлена текущая дата создания.