Skip to content

LOG: vaspahomov

Romutchio edited this page Jun 10, 2019 · 3 revisions

Проблема

  1. В обучении есть темы, которые нужно закреплять практикой. Каждому студенту требуется свое количество практики. Одна из таких тем - это оценка сложности алгоритмов.
  2. Студенты не всегда готовы выделить время за компьютером для дополнительной практики, но готовы тренироваться по несколько минут в день на телефоне, в моменты, когда есть свободное время без доступа к полноценному компьютеру. Сейчас они тратят это время на соц-сети, а могут тратить на обучение.

Цель

  1. В рамках командного проекта сделать бота для телеграмма, который позволит тренироваться в оценке сложности алгоритмов в свободное время прямо на своем телефоне. Для этого разработать механику, плавного увеличения сложности задач, с учетом успеваемости пользователя. Задания должны быть каждый раз уникальными и генерироваться на лету, чтобы исключить возможность запоминания уже увиденных вариантов.
  2. Обобщить бота так, чтобы его можно было использовать для других тем, отличных от оценки сложности алгоритмов.
  3. Обобщить код так, чтобы незначительными доработками можно было сделать его доступным через другие платформы для чат-ботов.

Архитектура решения

Решение построено из микросервисов, взаимодействующих между собой по протоколу HTTP.

Архитекрута

Core

Бизнес-логика викторины, построенная согласно принципам DDD.

Task API

Внешнее REST API для работы с задачами и уровнями. Построено на фреймворке ASP.NET Core.

Quiz API

Внешнее REST API для работы с викториной и пользователями. Построено на фреймворке ASP.NET Core.

Quiz Database

Интерфейс для работы с MongoDB: заданиями, пользователями и другой статистикой.

Telegram Bot

Построенный на ASP.NET Core webhook-bot для Telegram.

Telegram Users Database

Интерфейс для работы с MongoDB: состояниями пользователей и их данными для аутентификации в Task API.

Editor

Веб-сервис для редактирования и дизайна уровней викторины. Фронтенд написан на React JS, бекэнд - на ASP.NET Core. 

Постановка задачи

  1. Создать REST HTTP API для сервиса викторины

    • API для взаимодействия пользовательского интерфейса викторины с сервисом
    • API для редактиования заданий в сервисе
  2. Создание редактора для сервиса

  3. Спроектировать ролевую модель для редактора уровней

Решение

Архитектура HTTP API сервиса

Архитектура HTTP API основана на фрэймфорке ASP.NET

С помощью средств ASP.NET добавлены HTTP методы, которые позволяют взаимодействовать с сервисом викторины. Каждый запрос выполняется параллельно, что позволяет одновременно взаимодействовать с сервисом большому колличеству пользователей.

С сервисе викторины Quibble 3 ASP.NET контроллера:

  1. QuizServiceController
  2. TaskServiceController
  3. TemplateController

Во всех методах контроллеров:

  • Получение http запроса.
  • Валидация полученных данных
  • Вызов метода API из слоя Application, которому делегирована логика приложения.
  • Формирование DTO (Data transfer object)

DTO формируются с помощью AutoMapper, который импортирован Nuget пактеом в структуру проекта. Пример DTO, который сериализуется в JSON с помощью Newtonsoft из Nuget пакета.

using System;
using Newtonsoft.Json;

namespace QuizWebApp.Services.QuizService.DTO
{
    public class TopicInfoDTO
    {
        public TopicInfoDTO(string name, Guid id)
        {
            Name = name;
            Id = id;
        }

        [JsonProperty("id")] public Guid Id { get; }

        [JsonProperty("name")] public string Name { get; }
    }
}

На все контроллеры написана документация и подключен Swagger для выведения этой документации в удобном формате

QuizServiceController

Сервис предназначен для использования сервиса викторины пользовательскими интерфейсам. В данный момент реализованн Telegram бот, который пользуется данным api. Архитектура с использованием HTTP API позволяет создать несколько UI, которые будут пользоваться одним сервисом викторины. Все эти UI будут иметь общие базы уровней и пользователей. К викторине можно подключить другое web приложение, например web сайт.

Пример кода контроллера с несколькимим методами

namespace QuizWebApp.Services.QuizService
{
    [Route("api")]
    [ApiController]
    public class QuizServiceController : ControllerBase
    {
        private readonly IQuizService applicationApi;

        public QuizServiceController(IQuizService applicationApi)
        {
            this.applicationApi = applicationApi;
        }

        /// <summary>
        ///     Возвращает список всех тем.
        /// </summary>
        /// <remarks>
        ///     Sample request:
        ///     GET api/topics
        /// </remarks>
        /// <response code="200"> Возвращает список тем</response>
        [HttpGet("topics")]
        public ActionResult<IEnumerable<TopicInfoDTO>> GetTopics()
        {
            var topics = applicationApi.GetTopicsInfo().Value;
            return Ok(topics.Select(Mapper.Map<TopicInfoDTO>));
        }

        /// <summary>
        ///     Отправить ответ на сервер.
        /// </summary>
        /// <remarks>
        ///     Sample request:
        ///     POST api/0/sendAnswer
        ///     "O(1)"
        /// </remarks>
        /// <response code="200"> Возвращает bool верный ли ответ</response>
        /// <response code="403"> Данная операция не доступна пользователю</response>
        [HttpPost("{userId}/sendAnswer")]
        public ActionResult<bool> SendAnswer([FromBody] string answer, Guid userId)
        {
            var (isSuccess, _, result, error) = applicationApi.CheckAnswer(userId, answer);
            if (isSuccess)
                return Ok(result);
            if (error is AccessDeniedException)
                return Forbid();
            return InternalServerError(error.Message);
        }

        private ObjectResult InternalServerError(object value) =>
            StatusCode(StatusCodes.Status500InternalServerError, value);
    }
}

Запросы из QuizServiceController и их описание

Возвращает список всех тем.

GET /api/topics

Возвращает всех список уровней в теме.

GET /api/{topicId}/levels

Возвращает список уровней в теме доступных пользователю.

GET /api/{userId}/{topicId}/availableLevels

Возвращает прогресс пользователя.

GET /api/{topicId}/{levelId}/progress

Получить задачу.

GET /api/{topicId}/{levelId}/task

Возвращает следующую задачу из текущих темы и уровня пользователя.

GET /api/{userId}/nextTask

Возвращает подсказку на текущую задачу.

GET /api/{userId}/hint

Отправить ответ на сервер.

POST /api/{userId}/sendAnswer

TaskServiceController

Контроллер для редактирования коллекции заданий в сервисе викторины. С его помощью можно добавлять и удалять Topic, Level, TemplateGenerator в сервисе викторины. В данный момент к данному контроллеру подключен UI редактора уровней в виде web сайта. Также в контроллере есть возможость загружать целиком тему в форматах JSON и HJSON.

Запросы из TaskServiceController и их описание

Получить генераторы из уровня.

GET /service/{topicId}/{levelId}/templateGenerators

Добавляет в сервис новый пустую тему

POST /service/topic

Удаляет тему из сервиса

DELETE /service/topic/{topicId}

Добавляет в сервис пустой уровень

POST /service/{topicId}/level

Удаляет уровень из сервиса.

DELETE /service/{topicId}/level/{levelId}

Добавляет в сервис новый генератор

POST /service/{topicId}/{levelId}/templateGenerator

Удаляет генератор из сервиса.

DELETE /service/{topicId}/{levelId}/generator/{generatorId}

Загружает тему в формате HJSON в БД, автоматически проставляя ей айди.

POST /service/hjsonTopic

Получить весь топик в формате HJSON без айди

GET /service/{topicId}/hjsonTopic

Загружает тему в формате JSON в БД, автоматически проставляя ей айди.

POST /service/jsonTopic

Получить тему без айди в нем

GET /service/{topicId}/jsonTopic

TemplateController

В данном контроллере реализованы методы, помогающие при редактировании уровней. /renderTask позволяет предпросмотреть TemplateGenerator перед добавлением. Также есть метод, который позволяет посмотреть уже встроенные в сервис шаблоны, которые можно использовать в своих.

Запросы из TemplateController и их описание

Рендерит и возвращает задачу по шаблону из запроса

POST /templates/renderTask

Показывает набор существующих по умолчанию подстановок и их значений

GET /templates/substitutions/examples

LevelManager

Архитектура LevelManager

LevelManager основан на ASP.NET в качестве backend и ReactJS в качестве frontend.

Backend

Главной обязаностью backend части LevelManager'а является запуск ReactJS приложения. Конфигурация запуска прописывается в файле Startup.cs подобным образом.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseSpaStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            "default",
            "{controller}/{action=Index}/{id?}");
    });

    app.UseSpa(spa =>
    {
        spa.Options.SourcePath = "ClientApp";

        if (env.IsDevelopment()) spa.UseReactDevelopmentServer("start");
    });
}

Также на backend есть ProxyController который является прослойкой между API сервиса и приложением редактора. С помощью этого контроллера мы обходим CORS политику.

Архитекрута ProxyController

ProxyController принимает запросы с frontend части нашего приложения и отправляет запросы на HTTP API викторины с помощью интрефейса IQuizServiceExtended.cs, которому делегированы обязаности запросов.

Интерфейс IQuizServiceExtended.cs

using System;
using System.Collections.Generic;
using QuizRequestExtendedService.DTO;

namespace QuizRequestExtendedService
{
    public interface IQuizServiceExtended
    {
        IEnumerable<TopicDTO> GetTopics();

        IEnumerable<LevelDTO> GetLevels(Guid topicId);

        IEnumerable<AdminTemplateGeneratorDTO> GetTemplateGenerators(Guid topicId, Guid levelId);

        Guid AddEmptyTopic(EmptyTopicDTO topic);

        void DeleteTopic(Guid topicId);

        Guid AddEmptyLevel(Guid topicId, EmptyLevelDTO level);

        void DeleteLevel(Guid topicId, Guid levelId);

        Guid AddEmptyGenerator(Guid topicId, Guid levelId, TemplateGeneratorDTO topic);

        void DeleteTemplateGenerator(Guid topicId, Guid levelId, Guid generatorId);

        TemplateGeneratorDTO RenderTask(TemplateGeneratorForRenderDTO templateGenerator);
    }
}

Пример реализации запросов в классе Requester.cs, который наследует IQuizServiceExtended.cs.

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using QuizRequestExtendedService.DTO;
using RestSharp;

namespace QuizRequestExtendedService
{
    public class Requester : IQuizServiceExtended
    {
        private readonly string serverUri;
        private const int MaxRetries = 5;

        public Requester(string serverUri)
        {
            this.serverUri = serverUri;
        }

        public IEnumerable<TopicDTO> GetTopics()
        {
            var client = new RestClient(serverUri + "/api/topics");
            var content = SendGetRequest(client, Method.GET);
            var topics = JsonConvert.DeserializeObject<List<TopicDTO>>(content.Content);
            return topics;
        }

        public TemplateGeneratorDTO RenderTask(TemplateGeneratorForRenderDTO templateGenerator)
        {
            var client = new RestClient(serverUri + $"/templates/renderTask");
            var request = new RestRequest(Method.POST);
            request.AddJsonBody(templateGenerator);
            var content = client.Execute(request);
            var task = JsonConvert.DeserializeObject<TemplateGeneratorDTO>(content.Content);

            return task;
        }

        private IRestResponse SendGetRequest(IRestClient client, Method method, Parameter parameter = null)
        {
            var request = new RestRequest(method);
            if (parameter != null)
                request.AddParameter(parameter);
            for (var i = 0; i < MaxRetries; i++)
            {
                var response = client.Execute(request);
                if (response.IsSuccessful)
                    return response;
            }

            return null;
        }
    }
}

Frontend

Frontend чать приложния реализованна на ReactJS.

У Frontend чати редактора есть:

  • Страница авторизации
  • Компонент меню
  • Компоненты упраления (редактирование заданий)

Страница авторизации

Компонент меню

На Frontend реалозована правовая модель. Не все пользователи имеют доступ ко всем элементам управления.

Пример кода с реализацией правовой модели.

export const isAuthenticated = user => !!user;

export const isAllowed = (user, rights) =>
  rights.some(right => user.rights.includes(right));

export const hasRole = (user, roles) =>
  roles.some(role => user.roles.includes(role));

Пример ипользования isAllowed

{(isAllowed(this.state.user, ['can_edit_topics'])) ?
    <div className="center">    
        <button
            className="button1" 
            onClick={() => this.setState({createTopic: !this.state.createTopic})}
        >
            Создание Topic
        </button>
            {(this.state.createTopic) ? < CreateTopicForm/> : undefined}
        <br/>
    </div>
    : undefined}
Clone this wiki locally