СМИ о нас
Обрабатываем заказы
из интернет магазина
с помощью
RabbitMQ и TypeScript
#IT
Время чтения: 22 мин 27 сек
03 Октября 2019
#IT
Поделиться статьей
Подпишитесь на рассылку
Статья программиста IT-компании Lad Сергея Кучина.
Всем привет! Популярность интернет коммерции постоянно растет, как и доля информатизации всех смежных с торговлей видов деятельности. Вместе с этим растет и сложность обработки информации. Каждый заказ, сделанный клиентом интернет магазина, порождает за собой большое количество интеграций с различными сервисами. Такими сервисами могут быть сервисы обработки платежей, доставки, системы учета и лояльности. Каждый заказ должен быть оплачен, учтен, собран и доставлен, а также доступен для последующего анализа. Эту, и так не простую ситуацию, усложняет и тот факт, что пользователь интернет магазина не хочет долго и мучительно чего-то ждать при оформлении заказа. Отклик от интернет магазина должен быть быстрым, ведь каждая миллисекунда задержки увеличивает шанс потери клиента, а в последствии и прибыли. В этой статье я хочу рассказать про брокер сообщений RabbitMQ и как с его помощью можно организовать процесс обработки заказов используя Node.js и TypeScript. Добро пожаловать под кат.
Необходимая теория
Думаю, многие слышали про RabbitMQ, ведь первая open source версия этого брокера сообщений, основанного на протоколе AMQP, была выпущена аж в 2007 году. Брокер сообщений необходим для связи разных компонентов системы в единое целое, как клей необходим для реанимации разбитой вазы. С помощью брокера сообщений можно реализовать асинхронную обработку событий, поступающих в систему. Как раз такая асинхронная обработка заказов и нужна интернет магазину. Но для начала необходимо разобраться с основными компонентами RabbitMQ. У этого брокера есть три основных компонента, с помощью которых мы будем выстраивать процесс обработки:

• Message. Это минимальная единица информации в рамках брокера сообщений и нашего сервиса обработки, которая может быть обработана. Сам RabbitMQ хранит сообщения в бинарном виде, но для нашей системы и для статьи это не важно. Сообщения мы будем принимать и обрабатывать в виде JSON. Так же стоит упомянуть, что сообщения в RabbitMQ имеют заголовки. Они схожи с заголовками http запросов. Это ассоциативный массив, в который можно записать необходимую информацию.

• Message queue. Это очередь, в которой RabbitMQ хранит сообщения. На очередь сообщений могут быть подписаны один или несколько потребителей (consumer). Каждое сообщение из очереди rabbit распределяет по потребителям, используя алгоритм round-robin.

• Exchange. Это, как понятно из названия, точка обмена. К этой точке могут быть привязаны очереди или другие обменники. Точка обмена не хранит сообщения, основная ее функция — это маршрутизация сообщений в одну или несколько очередей, или такие же точки обмена. Каждая очередь или обменник привязывается по ключу маршрутизации (routing key). В RabbitMQ есть несколько разных типов обменников, которые влияют на то, как именно exchange будет маршрутизировать поступившее в него сообщение.

Для того, чтобы описать, как работают разные типы обменников, необходимо разобраться, что из себя представляют ключи маршрутизации. Ключ маршрутизации есть как у привязки (binding) очереди к обменнику, так и у самого сообщения. Routing key -это просто строка, поделенная на блоки. Каждый блок разделен точкой. Например, “notify.sendEmail.sendSms”. При этом для ключа маршрутизации сообщения можно задавать шаблоны с использованием специальных символов # и *. * — говорит что после точки может идти один любой блок, а вот после # может идти любое количество блоков. Например “notify.sendSms.*” или “notify.#”. Теперь можно переходить к типам точек обмена.

Есть четыре типа обменников:

• Fanout. Логика маршрутизации данного exchange'a проста, он перенаправляет поступившее сообщение во все очереди или обменники, которые привязаны к нему.
• Direct. Этот exchange перенаправляет сообщение в зависимости от того, совпадает ли routing key сообщения с routing key привязки.
• Topic. Exchange этого типа также как и Direct маршрутизирует сообщение в зависимости от routing key. Но в качестве ключа маршрутизации может выступать шаблон.
• Headers. Этот exchange, в отличие от остальных, использует для маршрутизации заголовки сообщений. При этом очереди к обменнику биндятся также с помощью ассоциативного массива. Логику, по которой обменник будет маршрутизировать сообщения, можно менять с помощью специального ключа “x-match“, который задается в ассоциативном массиве привязки. Ключу можно задать два значения all или any. Если значение all, то заголовки сообщения должны полностью совпадать с ассоциативным массивом привязки, если значение any, то значение должно совпадать хотя бы у одного ключа.
Это основные компоненты RabbitMQ. Более подробно об этих компонентах можно почитать в спецификации протокола AMQP. Далее мы будем проектировать и реализовывать систему обработки заказов на примере TypeScript, попутно разбираясь с настройками каждого компонента.
Проектирование
Для упрощения примера будем считать, что для успешной обработки интернет заказа у нас должен быть следующий функционал:

• Сохранять поступающие заказы
• Отправлять sms клиенту с номером заказа, а также статусом заказа
• Отправлять сообщение в службу курьерской доставки о новом заказе из нашего интернет магазина, если клиент выбрал этот способ доставки

Но мало реализовать этот функционал, ведь наш интернет магазин планирует расширять функционал и предоставлять в дальнейшем больше разных возможностей своим клиентам (а такое происходит всегда). Например, уведомлять клиента по email или предоставлять на выбор несколько способов доставки заказа. Из этого следует, что нам нужно спроектировать систему таким образом, чтобы добавлять функционал было просто.

Также стоит упомянуть, что я буду использовать шаблон отложенных сообщений для того, чтобы была возможность, при недоступности внешнего сервиса, повторять логику несколько раз. Про этот шаблон можно почитать тут

Чтобы более наглядно представить конечную цель, я нарисую схему.
Давайте разберемся по порядку, как устроен процесс обработки заказа на этой схеме. Схема разбита на блоки и разные цвета. Блоки белого цвета обозначают внешние сервисы, которые мы рассматривать не будем. Блоки серого цвета обозначают элементы RabbitMQ. Очереди и обменники. Зеленым цветом отражены блоки бизнес логики, которые необходимо реализовывать. Также каждый блок, имеющий отношение к нашей логике, пронумерован. Цифры обозначают процесс и подпроцесс в соответствии с порядком.

Первым делом, сообщение по HTTP API попадает в наш сервис. После этого мы должны присвоить номер заказу, сохранить заказ в базе данных со статусом “новый” и отправить ответ об успешном создании заказа, с его номером, обратно. Клиент, получив сообщение об успешном создании заказа, идет заниматься своими делами. Отправив положительный ответ, мы отправляем объект заказа в exchange постобработки, из которого он попадает в worker формирования routing key. Этот воркер, получив объект заказа из очереди, на его основе (есть ли в заказе email или телефон клиента, какой способ доставки был выбран) должен сформировать ключ маршрутизации заказа. Сформировав routing key, worker отправляет сообщение обратно в exchange постобработки, но теперь ключ маршрутизации у заказа изменился и обменник может отправить его уже по нужному маршруту. В зависимости от ключа, заказ может быть отправлен в exchange, который отвечает за уведомления, exchange интеграций или сразу в оба. А далее по такой же логике в очереди и воркеры.

Воркеры отправки sms и службы доставки будут пытаться обработать сообщение несколько раз. Количество таких попыток можно передать в переменной окружения. Но бесконечно обрабатывать сообщение не стоит, ведь ошибка может крыться в самом сообщении или логике воркера. Поэтому после превышения количества допустимых попыток сообщение будет удаляться из очередей и отправляться в хранилище ошибок, из которого его можно будет повторно отправить обратно на нужный уровень обработки.
Реализация
Для проверки реализации потребуется сам rabbit. Я рекомендую использовать для этой цели docker и официальный образ брокера. Установить и запустить контейнер можно следующей командой.

docker run -d --name rabbit -p 5672:5672 -e rabbitmq:3.7.15-management-alpine
Это образ с web интерфейсом, доступным на порту 15672, для удобной отладки.

Реализовывать задуманное будем с помощью TypeScript и библиотеки amqplib (реализация клиента RabbitMQ для Node.js) поэтому для начало необходимо описать несколько интерфейсов. Опишем интерфейсы заказа и сообщений, которые мы будем отправлять в rabbit.

// Интерфейс товара заказа
export interface Product {
  id: string;
  name: string;
  price: number;
}
 
// Общий интерфейс заказа
export interface Order {
  clientName: string;
  city: string;
  email?: string;
  phone?: string;
  products: Product[];
  totalSum: number;
  deliveryAddress?: string;
}
 
// Интерфейс заказа у которого есть номер телефона клиента
export interface OrderWithPhone extends Order {
  phone: string;
}
// Интерфейс заказа у которого есть адрес доставки
export interface OrderWithDeliveryAddress extends Order {
  deliveryAddress: string;
}
 
// Types Guard'ы для определения какой заказ к нам пришел
export const isOrderWithPhone = (order: Order): order is OrderWithPhone => Boolean(order.phone);
 
export const isOrderWithDeliveryAddress = (order: Order): order is OrderWithDeliveryAddress =>
  Boolean(order.deliveryAddress);
 
// Сообщение в рамках системы.
export interface Message<O extends Order> {
  errors: string[];
  retry: number;
  order: O;
 
// Интерфейс сообщения которое будет отправленно в хранилище ошибок
export interface FailOrder extends Message<Order> {
  exchange: string;
  routingKey: string;
}
Теперь нужно описать интерфейс конфигурации очередей и обменников, на основе которой будем строить структуру обработки в rabbit.

import { Types, ExchangeTypes } from '../constants';
import { Options } from 'amqplib';
 
// Типы объектов RabbitMQ которые мы будем использовать для конфигурации
export enum Types {
  QUEUE = 'queue',
  EXCHANGE = 'exchange',
}
 
// Типы обменников которые будем использовать
export enum ExchangeTypes {
  TOPIC = 'topic',
}
 
// Интерфейс описания очереди
export interface Queue {
  name: string;
  options: Options.AssertQueue;
}
 
// Интерфейс описания обменника
export interface Exchange {
  name: string;
  type: ExchangeTypes;
}
 
// Интерфейс описания привязки
export interface Binding {
  type: Types;
  destination: string;
  source: string;
  routingKey: string;
}
 
// Интерфейс конфигурации RabbitMQ
export interface PipelineConfig {
  queues: Queue[];
  exchanges: Exchange[];
  bindings: Binding[];
}
Описав основные компоненты системы, опишем конфигурацию, которая была нарисована на схеме с помощью объекта.

Queues

export default [
  // Очередь для сообщений для которых нужно сгенерировать routingKey
  {
    name: 'generateRoutingKey',
    options: {
      durable: true,
    },
  },
  // Очередь отправки sms
  {
    name: 'sendSms',
    options: {
      durable: true,
    },
  },
  // Очередь интеграции со службой доставки
  {
    name: 'delivery',
    options: {
      durable: true,
    },
  },
  // Отложенная очередь для сообщений которые ожидают повторной отправки sms
  {
    name: 'sendSmsHold',
    options: {
      durable: true,
      deadLetterExchange: 'notify',
      deadLetterRoutingKey: 'sendSms',
      messageTtl: 60000,
    },
  },
  // Отложенная очередь для сообщений которые ожидают повторной отправки в службу доставки
  {
    name: 'deliveryHold',
    options: {
      durable: true,
      deadLetterExchange: 'integrates',
      deadLetterRoutingKey: 'delivery',
      messageTtl: 60000,
    },
  },
];
При описании очередей используются следующие опции для очереди

• durable. По умолчанию все сообщения очереди хранятся в памяти. Следовательно, при перезагрузке брокера сообщения пропадут. Для избежания этого можно использовать эту опцию. С этой настройкой rabbit будет сбрасывать сообщения на диск. Но тут есть один нюанс. Чтобы сообщения сохранились после рестарта брокера, мало этой настройки, нужно, чтобы сообщения отправлялись в очередь с опцией persistent.
• messageTtl. Время жизни сообщения. Задаётся в миллисекундах
• deadLetterExchange. Имя обменника, куда сообщение отправится из очереди при истечении ее срока жизни
• deadLetterRoutingKey. RoutingKey, с которым сообщение будет отправлено в обменник из предыдущей опции

Exchanges

import { ExchangeTypes } from '../constants';
 
export default [
  {
    name: 'postprocessing',
    type: ExchangeTypes.TOPIC,
  },
  {
    name: 'notify',
    type: ExchangeTypes.TOPIC,
  },
  {
    name: 'integrates',
    type: ExchangeTypes.TOPIC,
  },
];
Bindings

import { Types } from '../constants';
 
export default [
  {
    type: Types.EXCHANGE,
    destination: 'notify',
    source: 'postprocessing',
    routingKey: '#.notify.#',
  },
  {
    type: Types.EXCHANGE,
    destination: 'integrates',
    source: 'postprocessing',
    routingKey: '#.integrates.#',
  },
  {
    type: Types.QUEUE,
    destination: 'generateRoutingKey',
    source: 'postprocessing',
    routingKey: 'generateRoutingKey',
  },
  {
    type: Types.QUEUE,
    destination: 'sendSms',
    source: 'notify',
    routingKey: '#.sendSms.#',
  },
  {
    type: Types.QUEUE,
    destination: 'delivery',
    source: 'integrates',
    routingKey: '#.delivery.#',
  },
  {
    type: Types.QUEUE,
    destination: 'sendSmsHold',
    source: 'notify',
    routingKey: 'sendSmsHold',
  },
  {
    type: Types.QUEUE,
    destination: 'deliveryHold',
    source: 'integrates',
    routingKey: 'deliveryHold',
  },
];
Полная конфигурация

import { PipelineConfig } from '../interfaces';
import exchanges from './exchanges';
import queues from './queues';
import bindings from './bindigs';
 
export const pipelineConfig: PipelineConfig = {
  exchanges,
  queues,
  bindings,
};
Для подключения к rabbit напишем класс.

import { connect, Connection, Channel } from 'amqplib';
 
export class RabbitConnect {
  private _uri: string;
  private _connection: Connection;
  private _chanel: Channel;
  constructor() {
    // Строка подключения к rabbit будет браться из окружения
    this._uri = process.env.RABBIT_URI || 'amqp://localhost';
  }
  protected async connect() {
    this._connection = await connect(this._uri);
    this._chanel = await this._connection.createChannel();
  }
  protected async disconnect() {
    await this._chanel.close();
    return this._connection.close();
  }
  protected get chanel() {
    return this._chanel;
  }
}
Напишем класс Pipeline, который при старте будет создавать всю необходимую инфраструктуру в rabbit по описанной ранее конфигурации.

import { RabbitConnect } from './RabbitConnect';
import { PipelineConfig } from './interfaces';
import { Types } from './constants';
 
export class Pipeline extends RabbitConnect {
  private _pipeline: PipelineConfig;
  constructor(pipelineConfig: PipelineConfig) {
    super();
    this._pipeline = pipelineConfig;
  }
  public async create() {
    try {
      await this.connect();
 
      // Создаем очереди
      const createQueues = this._pipeline.queues.map(queue =>
        this.chanel.assertQueue(queue.name, queue.options),
      ) as PromiseLike<any>[];
      // Создаём обменники
      const createExchanges = this._pipeline.exchanges.map(exchange =>
        this.chanel.assertExchange(exchange.name, exchange.type),
      ) as PromiseLike<any>[];
 
      await Promise.all([...createQueues, ...createExchanges]);
      // После создания необходимых компонентов создаём биндинги
      const createBindings = this._pipeline.bindings.map(binding => {
        if (binding.type === Types.QUEUE) {
          return this.chanel.bindQueue(binding.destination, binding.source, binding.routingKey);
        }
        return this.chanel.bindExchange(binding.destination, binding.source, binding.routingKey);
      });
 
      await Promise.all(createBindings);
      return this.disconnect();
    } catch (error) {
      console.error(error);
      throw new Error(error);
    }
  }
}
Теперь напишем абстрактный класс воркера с общим для всех воркеров функционалом, от которого можно будет наследоваться.

import { RabbitConnect } from './RabbitConnect';
import { Message, Order, FailOrder } from './interfaces';
import { ConsumeMessage } from 'amqplib';
 
export interface WorkerParams {
  maxRetry?: number; // Максимальное количество повторов обработки
  active: string; // Имя активной очереди
  exchange: string; // Имя обменника из которого пришло сообщение
  holdKey?: string; // Ключ маршрутизации для отложенной очереди
}
 
export abstract class Worker<M extends Order> extends RabbitConnect {
  private _maxRetry: number;
  private _active: string;
  private _holdKey: string | undefined;
  protected exchange: string;
  private _currentMessage: Message<M>;
  private _currentConsumeMessage: ConsumeMessage;
  constructor({ active, holdKey, exchange, maxRetry }: WorkerParams) {
    super();
    this._maxRetry = maxRetry || 0;
    this._active = active;
    this._holdKey = holdKey;
    this.exchange = exchange;
  }
  public async subscribe() {
    await this.connect();
    this.chanel.consume(this._active, this.checkMessage.bind(this));
  }
  // Метод проверки для сообщений у которых превышен лимит повторов
  private async checkMessage(message: ConsumeMessage) {
    this._currentConsumeMessage = message;
    const orderMessage: Message<M> = JSON.parse(message.content.toString());
    if (orderMessage.retry >= this._maxRetry) {
      await this.sendToErrorStorage('Превышен лимит попыток');
    }
    this._currentMessage = orderMessage;
    // Если количество попыток не превышено вызываем метод с бизнес логикой
    await this.handler(orderMessage.order || orderMessage);
  }
  // Метод отправки сообщения в хранилище ошибок
  protected async sendToErrorStorage(error: string) {
    const message: FailOrder = {
      order: this._currentMessage.order,
      errors: [...this._currentMessage.errors, error],
      retry: this._currentMessage.retry + 1,
      exchange: this.exchange,
      routingKey: this._active
    };
    console.log('Отправка в хранилище ошибок', message);
    this.ack();
  }
  // Метод отправки сообщения в отложенную очередь
  protected async hold(error: string) {
    if (!this._holdKey) {
      return;
    }
    const orderMessage = {
      order: this._currentMessage.order,
      errors: [...this._currentMessage.errors, error],
      retry: this._currentMessage.retry + 1,
    };
    const orderData = Buffer.from(JSON.stringify(orderMessage));
    return this.chanel.publish(this.exchange, this._holdKey, orderData);
  }
  // Метод подтверждения удачной обработки сообщения
  protected async ack() {
    return this.chanel.ack(this._currentConsumeMessage);
  }
  protected abstract handler(message: M): void;
}
По умолчанию rabbit требует подтверждение уcпешной обработки сообщения от воркера. Для этого у канала подключения есть метод ack. Если воркер не смог обработать сообщение, то существует метод nack, который говорит rabbit'у, чтобы он отправил сообщение другому воркеру.

Теперь мы можем написать несколько простых воркеров из схемы.

Воркер генерации ключа маршрутизации.
import { Worker } from '../Worker';
import {
  isOrderWithPhone,
  isOrderWithDeliveryAddress,
  Order,
  Message,
} from '../interfaces';
import { Keys } from '../constants';
 
export class GenerateRoutingKey extends Worker<Order> {
  constructor() {
    super({
      active: 'generateRoutingKey',
      exchange: 'postprocessing',
    });
  }
  protected async handler(order: Order) {
    try {
      const routingKey: string[] = [];
      if (isOrderWithPhone(order)) {
        routingKey.push(Keys.SEND_SMS);
      }
      if (isOrderWithDeliveryAddress(order)) {
        routingKey.push(Keys.SEND_TO_DELIVERY);
      }
      const message: Message<Order> = {
        retry: 0,
        errors: [],
        order,
      };
      await this.chanel.publish(
        this.exchange,
        routingKey.join('.'),
        Buffer.from(JSON.stringify(message)),
      );
      await this.ack();
    } catch (error) {
      console.error(error);
      await this.sendToErrorStorage(error);
    }
  }
}
Воркеры отправки sms.

import { Worker } from '../Worker';
import { OrderWithPhone } from '../interfaces';
 
export class SendSms extends Worker<OrderWithPhone> {
  constructor() {
    super({
      active: 'sendSms',
      exchange: 'notify',
      holdKey: 'sendSmsHold',
      maxRetry: process.env.MAX_RETRY ? parseInt(process.env.MAX_RETRY) : 5,
    });
  }
  protected async handler(message: OrderWithPhone) {
    try {
      console.log('Отправка sms на номер: ', message.phone);
      this.ack();
    } catch (error) {
      console.error(error);
      await this.hold(error);
    }
  }
}
Воркер интеграции со службой доставки.

import { Worker } from '../Worker';
import { OrderWithDeliveryAddress } from '../interfaces';
 
export class Delivery extends Worker<OrderWithDeliveryAddress> {
  constructor() {
    super({
      active: 'delivery',
      exchange: 'interates',
      holdKey: 'deliveryHold',
      maxRetry: process.env.MAX_RETRY ? parseInt(process.env.MAX_RETRY) : 5,
    });
  }
  protected async handler(message: OrderWithDeliveryAddress) {
    try {
      console.log('Отправка заказа в службу доставки на адрес: ', message.deliveryAddress);
      this.ack();
    } catch (error) {
      console.error(error);
      await this.hold(error);
    }
  }
}
Точка входа в приложение.

import { Pipeline } from './Pipeline';
import { pipelineConfig } from './pipeline';
import { GenerateRoutingKey } from './workers/GenerateRoutingKey';
import { SendSms } from './workers/SendSms';
import { Delivery } from './workers/Delivery';
 
(async () => {
  try {
    const pipeline = new Pipeline(pipelineConfig);
    const generateRoutingKey = new GenerateRoutingKey();
    const sendSms = new SendSms();
    const delivery = new Delivery();
    await pipeline.create();
    await Promise.all([generateRoutingKey.subscribe(), sendSms.subscribe(), delivery.subscribe()]);
  } catch (error) {
    console.error(error);
    process.exit(1);
  }
})();
Приводить пример кода класса записи заказа в базу и генерации номера интернет заказа я не буду. Это выходит за рамки данной статьи. Для проверки кода можно воспользоваться веб интерфейсом rabbit'а, отправив в обменник posrprocessing json заказа.
Заключение
Такая схема построения обработки интернет заказа позволяет легко масштабировать систему. Нам не составит труда добавить в эту схему несколько очередей и воркеров, чтобы добавить нужный функционал. Например, можно добавить отправку уведомлений на email или отправку заказа для учета в 1С. Преобразованная схема будет выглядеть так:
Надеюсь, вам понравилась статья. Буду рад любым комментариям и критике. Весь представленный код можно найти на github
Другие статьи из раздела СМИ о нас
07 Октября
СМИ о нас  
Почему обучение технического писателя — это нетривиальная задача
01 Октября
СМИ о нас  
IT-компания Lad помогает бизнесу и государству управлять HR-капиталом
28 Сентября
СМИ о нас  
ООО «Платформа строительных сервисов» — серебряный партнер конференции #ГИСОГД2021
27 Сентября
СМИ о нас  
Проект управления HR капиталом региона представлен на «ПРОФ-IT»
27 Сентября
СМИ о нас  
Список региональных IT-проектов – победителей конкурса «ПРОФ-IT.2021»
27 Сентября
СМИ о нас  
IX Всероссийский форум «ПРОФ-IT» — фотолента
СМИ о нас  
ТВ: На старт благотворительного забега “Открытые сердца” вышли более 200 человек
30 Августа
СМИ о нас  
Топ-17 крупнейших ИT-компаний Нижнего Новгорода
18 Августа
СМИ о нас  
Нижегородская область лидирует по доступности мер поддержки IT-компаний
05 Августа
СМИ о нас  
Прокачиваем анимацию с react-native-reanimated. Часть 1
02 Августа
СМИ о нас  
Чем занимается технический писатель: опыт IT-компании Lad
30 Июля
СМИ о нас  
Пишем свой dependency free WebSocket сервер на Node.js
27 Июля
СМИ о нас  
Сколько стоит рекомендация IT-специалиста в Нижнем. Аналитика
16 Июля
СМИ о нас  
IT-компания Lad вошла в ТОП-5 работодателей по найму Junior-разработчиков
06 Июля
СМИ о нас  
Технический писатель: с чего начать
28 Июня
СМИ о нас  
10 эко-приложений, которые помогают спасать природу
27 Июня
СМИ о нас  
IT-кампус на 7 000 студентов создадут в Нижнем Новгороде
23 Июня
СМИ о нас  
В Нижнем Новгороде пройдет паблик-ток о развитии IT-сферы
23 Июня
СМИ о нас  
В Арсенале обсудят IT в Нижегородской области
16 Июня
СМИ о нас  
Обучающая программа «Веб-разработка» от HiBrain стартовала при ННГУ
15 Июня
СМИ о нас  
Оплатить ЖКХ в режиме онлайн теперь можно на портале «Карты жителя Нижегородской области»
10 Июня
СМИ о нас  
«Прекрасная идея» или напрасные затраты? IT-бизнес — о проекте редевелопмента Започаинья
10 Июня
СМИ о нас  
Нижегородские IT-компании смогут побороться за победу во всероссийском конкурсе «IT-проект: Back To The Product»
03 Июня
СМИ о нас  
Скидки и сертификаты стали доступны на «Карте жителя Нижегородской области»
01 Июня
СМИ о нас  
Фестиваль детских инновационных проектов прошел в технопарке «Кванториум Нижний Новгород»
26 Мая
СМИ о нас  
Нижегородские школьники разработали маску дополненной реальности для сервиса «Карта жителя Нижегородской области»
20 Мая
СМИ о нас  
Как интегратор 1С балуется плюшками
17 Мая
СМИ о нас  
Историческая викторина в честь 800-летия Нижнего Новгорода стартовала на “Карте жителя”
12 Мая
СМИ о нас  
Жители региона смогут получать цифровые квитанции через сервис «Карта жителя Нижегородской области
12 Мая
СМИ о нас  
Квитанции об оплате услуг ЖКХ можно получить на портале «Карта жителя»
27 Апреля
СМИ о нас  
Джентльменский набор приложений для смартфона в 2021 году
23 Апреля
СМИ о нас  
IT-компания Lad вошла в число крупнейших работодателей рынка IT в Нижнем Новгороде
16 Апреля
СМИ о нас  
Какой кошелек выбрать?
05 Апреля
СМИ о нас  
Формы платформы
04 Апреля
СМИ о нас  
На 30% Минстрой России прогнозирует повышение индекса IQ городов к 2024 году
01 Апреля
СМИ о нас  
К 2024 году в России планируется на 30% повысить индекс цифровизации городов
22 Марта
СМИ о нас  
Эко-квест «800 шагов к чистому городу»
18 Марта
СМИ о нас  
Все школы в России обеспечат доступом к интернету в этом году
06 Марта
СМИ о нас  
Команды из Нижегородской области стали победителями межрегионального онлайн-хакатона
06 Марта
СМИ о нас  
Кейс «Instagram-маски в честь 800-летия Нижнего Новгорода» от IT-компании Lad
01 Марта
СМИ о нас  
Где работать в ИТ в 2021: Lad
28 Февраля
СМИ о нас  
Нижегородский онлайн-хакатон «VRARHack52» соберет юных разработчиков со всей страны
22 Февраля
СМИ о нас  
Диагностируем проблемы в микросервисной архитектуре на Node.js с помощью OpenTracing и Jaeger
25 Декабря
СМИ о нас  
В российских школах заменяют Word и Excel на отечественные аналоги
24 Декабря
СМИ о нас  
Дмитрий Петров о налоговых льготах для IT-сферы
СМИ о нас  
Нижегородские IT-компании получат налоговые льготы
22 Декабря
СМИ о нас  
Профит для бизнеса от работы контактного центра на удаленке: кейс компании Lad
10 Декабря
СМИ о нас  
Росатом интегрирует решения российских разработчиков в проекты Smart City
07 Декабря
СМИ о нас  
Портал "Карта жителя Нижегородской области" стал удобнее
06 Декабря
СМИ о нас  
На портале «Карта жителя Нижегородской области» обновлен профиль пользователя
30 Ноября
СМИ о нас  
iCluster обсудит лучшие практики импортозамещения
27 Ноября
СМИ о нас  
Lad: цифровая трансформация бизнеса
19 Ноября
СМИ о нас  
IT-компания Lad поделится лучшими практиками
18 Ноября
СМИ о нас  
Компания Lad проведет день открытых дверей и расскажет о трендах IT-разработки
14 Ноября
СМИ о нас  
Участники регионального iCluster расскажут об отраслевых трендах
29 Октября
СМИ о нас  
О будущем проекта "Карта жителя Нижегородской области"
22 Октября
СМИ о нас  
Эксперт Lad дал комментарий Tadviser об импортозамещении офисного софта
14 Октября
СМИ о нас  
Как ИТ-специалисту устроиться на работу в хорошую компанию
07 Октября
СМИ о нас  
Карту жителя Нижегородской области начнут тестировать в октябре
06 Октября
СМИ о нас  
К сервису «Карта жителя Нижегородской области» присоединились первые банки-партнеры
01 Октября
СМИ о нас  
Пакет офисных решений "Р7-Офис" поступит в школы Ингушетии
01 Октября
СМИ о нас  
Ингушетия получит 3 тыс. лицензий на российское ПО для системы образования
25 Августа
СМИ о нас  
НБД-Банк и IT-компания Lad предлагают малому бизнесу онлайн-кассы в аренду
18 Июня
СМИ о нас  
Алтайский край переходит на российское ПО
06 Мая
СМИ о нас  
Work must go on!
03 Апреля
СМИ о нас  
IT-компании предлагают свои продукты бесплатно
26 Марта
СМИ о нас  
ПСС на Digital.Forum Construction 2020
24 Марта
СМИ о нас  
Быстрая цифровизация строительной отрасли
16 Марта
СМИ о нас  
Партнерская акция IT-компании Lad и НБД-Банка
13 Февраля
СМИ о нас  
Отечественное ПО для белгородских школьников
СМИ о нас  
Платформа HiBrain - разработка IT-компании Lad
СМИ о нас  
Digital Summit 2019
24 Октября
СМИ о нас  
В Нижнем Новгороде iCluster провёл IT-фестиваль "iFest 2019"
21 Октября
СМИ о нас  
Lad на фестивале программистов IFest
27 Сентября
СМИ о нас  
Lad примет участие в Digital Summit 2019
СМИ о нас  
Как IT-технологии изменили жизнь нижегородцев?
06 Августа
СМИ о нас  
В Нижегородской области запустят ПСС
07 Июня
СМИ о нас  
На ПМЭФ презентованы проекты IT-компании Lad
06 Июня
СМИ о нас  
На ПМЭФ презентованы ключевые нижегородские ИТ проекты
29 Мая
СМИ о нас  
В Нижнем Новгороде запустили цифровую платформу IT-образования HiBrain
24 Апреля
СМИ о нас  
Kari втрое ускорила оформление рассрочки
12 Февраля
СМИ о нас  
Цифровизация Retail-отрасли: актуальные тренды
23 Ноября
СМИ о нас  
Лучшие IT-проекты выбрали в Нижнем Новгороде
Поделитесь вашим мнением
Ответ успешно отправлен Мы свяжемся с вами в ближайшее время

Файлы cookies

Мы используем файлы cookies. Это необходимо для анализа трафика и корректной работы сайта. Продолжая работу с сайтом, вы подтверждаете свое согласие на применение этих технологий.

подробнее хорошо