Виджет CDEK с бэкендом на .NET

от автора

Всем привет. Некоторое время назад стояла задача интегрировать виджет CDEK в сайт на .NET. Код виджета доступен на github: фронт (ts) + бэкенд (php). При переносе на .NET с фронтом проблем нет. Кроме того, есть готовый скрипт, который можно подключить с cdn. Но при этом есть существенное ограничение для бэкенда: данный скрипт работает только с php.

В данной статье показано, как перевести виджет на бэкенд .NET. При этом фронтенд код остается неизменным. Прежде, чем начать, хочу предупредить, что данное решение никак не связано с официальной версией виджета и не поддерживается командой CDEK. В любой момент CDEK может изменить свой код без сохранения обратной совместимости, и решение, представленное здесь, может перестать работать. Тем не менее, думаю (вернее, мне хотелось бы так думать 🙂 ), что информация, представленная в данной статье, может быть полезной как с точки зрения конечного результата, так и в образовательных целях.

По умолчанию фронт виджета посылает запросы на файл service.php (оригинальный код), находящийся в корне сайта. Поэтому задача в конечном счете свелась к двум подзадачам:

  1. добавить имитацию service.php для запросов с фронта в .NET сайт

  2. перевести код service.php на .NET

Первая подзадача решается достаточно просто через добавление промежуточного слоя (middleware) для запросов на service.php. В типовом ASP.NET Core приложении это можно сделать, добавив следующий код в Program.cs:

app.UseWhen(     context => context.Request.Path.ToString().ToLower().Contains("/service.php"),     appBranch => {         appBranch.UseCdekMiddleware();     });

С данной конструкцией запросы на service.php будут обрабатываться нашим промежуточным слоем CdekMiddleware (обращаю внимание, что все это мы делаем внутри .NET сайта, не имеющим ничего общего с php. Т.е. php не нужно устанавливать на ваш сервер, где крутится .NET сайт). Соответственно код метода-расширения UseCdekMiddleware() и самого промежуточного слоя:

internal class CdekMiddleware {         public CdekMiddleware(RequestDelegate next)     {     }      public async Task Invoke(HttpContext context, ICdekService cdekService)     {         var requestParams = await this.getRequestParams(context);          object action = null;         requestParams?.TryGetValue("action", out action);         if (string.IsNullOrEmpty(action as string))         {             await this.sendValidationError(context, "Action is required");             return;         }          if (string.Compare(action as string, "offices", true) == 0)         {             var result = cdekService.GetOffices(requestParams);             await this.writeResponseAsync(context, result);             return;         }         if (string.Compare(action as string, "calculate", true) == 0)         {             var calculations = cdekService.Calculate(requestParams);             await this.writeResponseAsync(context, calculations);             return;         }          await this.sendValidationError(context, "Unknown action");     }      private async Task<Dictionary<string, object>> getRequestParams(HttpContext ctx)     {         var data = new Dictionary<string, object>();          foreach (var kv in ctx.Request.Query)         {             data[kv.Key] = kv.Value.ToString();         }          var stream = ctx.Request.Body;         string json = await new StreamReader(stream).ReadToEndAsync();         if (!string.IsNullOrEmpty(json))         {             var body = JsonConvert.DeserializeObject<ExpandoObject>(json) as IDictionary<String, Object>;             body?.ToList().ForEach(kv => { data[kv.Key] = kv.Value; });         }          return data;     }      private async Task sendValidationError(HttpContext context, string msg)     {         context.Response.StatusCode = StatusCodes.Status400BadRequest;         await this.writeResponseAsync(context, new CdekResponse{Data = msg});     }      private async Task writeResponseAsync(HttpContext context, CdekResponse resp)     {         resp?.Headers?.ToList().ForEach(kv =>         {             context.Response.Headers.TryAdd(kv.name, kv.value);         });         await context.Response.WriteAsJsonAsync((object)resp?.Data);     } }  internal static class CdeknMiddlewareExtension {     public static IApplicationBuilder UseCdekMiddleware(this IApplicationBuilder builder)     {         return builder.UseMiddleware<CdekMiddleware>();     } }

При переносе кода старался сделать его как можно ближе к оригиналу, используя мои (скромные) знания php. Как видно из анализа кода оригинального виджета, service.php по сути является прокси между фронтом и API CDEK, который работает в вашем домене, чтобы при подключении фронта не было проблем с CORS. Поэтому вместе непосредственно с данными в формате json, в ответ сервера также добавляются кастомные HTTP заголовки, пришедшие из API (заголовки, названия которых начинаются с X-). Так например виджет получает список пунктов вывоза с поддержкой постраничой навигации (используются заголовки X-Current-Page, X-Total-Elements, X-Total-Pages).

Внутри CdekMiddleware вызывает CdekService для получения списка пунктов вывоза и расчета стоимости доставки. Код CdekService вместе с рабочим тестовым приложением на .NET8 доступен на github. Инструкция для запуска приложения также найдете на github. Для запуска вам потребуюся clientId/clientSecret для API CDEK (тестовые ключи можно найти на сайте документации CDEK) и ключ API Яндекс Карт (необходимо сгенерировать в кабинете разработчика Яндекс).

Настройки виджета в приложении взяты из примера CDEK с указанием всех настроек виджета. Если все сделано правильно, то при запуске появится виджет с возможностью выбора пункта вывоза и расчета стоимости доставки:

Отображение пунктов вывоза

Отображение пунктов вывоза
Расчет стоимости доставки

Расчет стоимости доставки


ссылка на оригинал статьи https://habr.com/ru/articles/894802/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *