Рубрики
Без рубрики

Второй раунд улучшений приложения Reddit

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

Автор оригинала: baeldung.

1. Обзор

Давайте продолжим наше текущее тематическое исследование веб-приложения Reddit с новым раундом улучшений с целью сделать приложение более удобным для пользователя и простым в использовании.

2. Разбиение на страницы Запланированных Сообщений

Во – первых , давайте перечислим запланированные сообщения с разбиением на страницы , чтобы все это было легче просматривать и понимать.

2.1. Операции С Разбиением На Страницы

Мы будем использовать данные Spring для создания нужной нам операции, хорошо используя интерфейс Pageable для извлечения запланированных сообщений пользователя:

public interface PostRepository extends JpaRepository {
    Page findByUser(User user, Pageable pageable);
}

И вот наш метод контроллера получить запланированные сообщения() :

private static final int PAGE_SIZE = 10;

@RequestMapping("/scheduledPosts")
@ResponseBody
public List getScheduledPosts(
  @RequestParam(value = "page", required = false) int page) {
    User user = getCurrentUser();
    Page posts = 
      postReopsitory.findByUser(user, new PageRequest(page, PAGE_SIZE));
    
    return posts.getContent();
}

2.2. Отображение Сообщений С Разбивкой По Страницам

Теперь – давайте реализуем простой контроль разбиения на страницы в интерфейсе:

Post title

И вот как мы загружаем страницы с помощью простого jQuery:

$(function(){ 
    loadPage(0); 
}); 

var currentPage = 0;
function loadNext(){ 
    loadPage(currentPage+1);
} 

function loadPrev(){ 
    loadPage(currentPage-1); 
}

function loadPage(page){
    currentPage = page;
    $('table').children().not(':first').remove();
    $.get("api/scheduledPosts?page="+page, function(data){
        $.each(data, function( index, post ) {
            $('.table').append(''+post.title+'');
        });
    });
}

По мере продвижения вперед эта ручная таблица будет быстро заменена более зрелым табличным плагином, но пока это работает просто отлично.

3. Покажите страницу входа для пользователей, не вошедших в систему

Когда пользователь обращается к корневому каталогу, они должны получать разные страницы, если они вошли в систему или нет .

Если пользователь вошел в систему, он должен увидеть свою домашнюю страницу/панель мониторинга. Если они не вошли в систему – они должны увидеть страницу входа в систему:

@RequestMapping("/")
public String homePage() {
    if (SecurityContextHolder.getContext().getAuthentication() != null) {
        return "home";
    }
    return "index";
}

4. Дополнительные параметры для повторной отправки

Удаление и повторная отправка сообщений в Reddit-это полезная и высокоэффективная функция. Тем не менее, мы хотим быть осторожными с этим и иметь полный контроль над тем, когда мы должны и когда мы не должны этого делать.

Например, мы можем не захотеть удалять сообщение, если в нем уже есть комментарии. В конце концов, комментарии важны, и мы хотим уважать платформу и людей, комментирующих этот пост.

Итак, это первая небольшая, но очень полезная функция, которую мы добавим – новая опция, которая позволит нам удалять сообщение только в том случае, если в нем нет комментариев.

Еще один очень интересный вопрос, на который нужно ответить: если сообщение повторно отправляется сколько угодно раз, но все еще не получает необходимой тяги – оставляем ли мы его включенным после последней попытки или нет? Ну, как и на все интересные вопросы, ответ здесь – “это зависит”. Если это обычный пост, мы могли бы просто назвать его днем и оставить его. Однако, если это очень важный пост, и мы действительно хотим убедиться, что он получит некоторую поддержку, мы можем удалить его в конце.

Итак, это вторая небольшая, но очень удобная функция, которую мы создадим здесь.

Наконец, как насчет спорных постов? Сообщение может иметь 2 голоса на reddit, потому что там оно должно иметь положительные голоса, или потому, что оно имеет 100 положительных и 98 отрицательных голосов. Первый вариант означает, что он не получает тяги, в то время как второй означает, что он получает много тяги и что голосование разделено.

Итак, это третья небольшая функция, которую мы собираемся добавить – новая опция, позволяющая учитывать соотношение голосов вверх и вниз при определении того, нужно ли нам удалять сообщение или нет.

4.1. Почтовая Организация

Во-первых, нам нужно изменить наш Post объект:

@Entity
public class Post {
    ...
    private int minUpvoteRatio;
    private boolean keepIfHasComments;
    private boolean deleteAfterLastAttempt;
}

Вот 3 поля:

  • min Upvote Ratio : Минимальное соотношение голосов, которого пользователь хочет достичь своим сообщением – соотношение голосов представляет, сколько % от общего числа голосов набирает ara,]
  • keep If HasComments : Определите, хочет ли пользователь сохранить свой пост, если у него есть комментарии, несмотря на то, что он не набрал необходимого балла.
  • удалить после последней попытки : Определите, хочет ли пользователь удалить сообщение после окончания последней попытки, не набрав необходимого балла.

4.2. Планировщик

Давайте теперь интегрируем эти интересные новые опции в планировщик:

@Scheduled(fixedRate = 3 * 60 * 1000)
public void checkAndDeleteAll() {
    List submitted = 
      postReopsitory.findByRedditIDNotNullAndNoOfAttemptsAndDeleteAfterLastAttemptTrue(0);
    
    for (Post post : submitted) {
        checkAndDelete(post);
    }
}

На более интересной части – фактическая логика checkAndDelete() :

private void checkAndDelete(Post post) {
    if (didIntervalPass(post.getSubmissionDate(), post.getTimeInterval())) {
        if (didPostGoalFail(post)) {
            deletePost(post.getRedditID());
            post.setSubmissionResponse("Consumed Attempts without reaching score");
            post.setRedditID(null);
            postReopsitory.save(post);
        } else {
            post.setNoOfAttempts(0);
            post.setRedditID(null);
            postReopsitory.save(post);
        }
    }
}

И вот didPostGoalFail() реализация – проверка, не достигло ли сообщение заданной цели/результата :

private boolean didPostGoalFail(Post post) {
    PostScores postScores = getPostScores(post);
    int score = postScores.getScore();
    int upvoteRatio = postScores.getUpvoteRatio();
    int noOfComments = postScores.getNoOfComments();
    return (((score < post.getMinScoreRequired()) || 
             (upvoteRatio < post.getMinUpvoteRatio())) && 
           !((noOfComments > 0) && post.isKeepIfHasComments()));
}

Нам также необходимо изменить логику, которая извлекает информацию Post из Reddit, чтобы убедиться, что мы собираем больше данных:

public PostScores getPostScores(Post post) {
    JsonNode node = restTemplate.getForObject(
      "http://www.reddit.com/r/" + post.getSubreddit() + 
      "/comments/" + post.getRedditID() + ".json", JsonNode.class);
    PostScores postScores = new PostScores();

    node = node.get(0).get("data").get("children").get(0).get("data");
    postScores.setScore(node.get("score").asInt());
    
    double ratio = node.get("upvote_ratio").asDouble();
    postScores.setUpvoteRatio((int) (ratio * 100));
    
    postScores.setNoOfComments(node.get("num_comments").asInt());
    
    return postScores;
}

Мы используем простой объект value для представления результатов по мере их извлечения из API Reddit:

public class PostScores {
    private int score;
    private int upvoteRatio;
    private int noOfComments;
}

Наконец, нам нужно изменить check И ReSubmit () , чтобы установить их успешно повторно отправленный идентификатор reddit в null :

private void checkAndReSubmit(Post post) {
    if (didIntervalPass(post.getSubmissionDate(), post.getTimeInterval())) {
        if (didPostGoalFail(post)) {
            deletePost(post.getRedditID());
            resetPost(post);
        } else {
            post.setNoOfAttempts(0);
            post.setRedditID(null);
            postReopsitory.save(post);
        }
    }
}

Обратите внимание, что:

  • проверьте и удалите все() : выполняется каждые 3 минуты, чтобы узнать, израсходовали ли какие-либо сообщения свои попытки и могут быть удалены
  • получить баллы за публикацию() : вернуть {балл за публикацию, соотношение голосов, количество комментариев}

4.3. Измените Страницу Расписания

Нам нужно добавить новые модификации в наш schedulePostForm.html :



5. Отправляйте по Электронной почте Важные Журналы

Затем мы реализуем быструю, но очень полезную настройку в нашей конфигурации обратной связи – отправка по электронной почте важных журналов ( ОШИБКА уровень) . Это, конечно, очень удобно, чтобы легко отслеживать ошибки на ранних этапах жизненного цикла приложения.

Во-первых, мы добавим несколько необходимых зависимостей к вашему pom.xml :


    javax.activation
    activation
    1.1.1


    javax.mail
    mail
    1.4.1

Затем мы добавим SMTPAppender к вашему logback.xml :



    
        
            ERROR
            ACCEPT
            DENY
        

        smtp.example.com
        [email protected]
        [email protected]
        [email protected]
        password
        %logger{20} - %m
        
    

    
        
        
    

И это все – теперь развернутое приложение будет отправлять по электронной почте любую проблему, когда это произойдет.

6. Кэширование Субреддитов

Оказывается, автоматическое заполнение субреддитов дорого . Каждый раз, когда пользователь начинает вводить субреддит при планировании публикации – нам нужно нажать на API Reddit, чтобы получить эти субреддиты и показать пользователю некоторые предложения. Не идеально.

Вместо вызова API Reddit – мы просто кэшируем популярные субреддиты и используем их для автозаполнения.

6.1. Получение Субредитов

Во-первых, давайте извлекем самые популярные субреддиты и сохраним их в обычный файл:

public void getAllSubreddits() {
    JsonNode node;
    String srAfter = "";
    FileWriter writer = null;
    try {
        writer = new FileWriter("src/main/resources/subreddits.csv");
        for (int i = 0; i < 20; i++) {
            node = restTemplate.getForObject(
              "http://www.reddit.com/" + "subreddits/popular.json?limit=100&after=" + srAfter, 
              JsonNode.class);
            srAfter = node.get("data").get("after").asText();
            node = node.get("data").get("children");
            for (JsonNode child : node) {
                writer.append(child.get("data").get("display_name").asText() + ",");
            }
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                logger.error("Error while getting subreddits", e);
            }
        }
        writer.close();
    } catch (Exception e) {
        logger.error("Error while getting subreddits", e);
    }
}

Является ли это зрелой реализацией? Нет. Нужно ли нам что-нибудь еще? Нет, мы этого не делаем. Нам нужно двигаться дальше.

6.2. Автозаполнение Субреддита

Далее, давайте убедимся, что субреддиты загружаются в память при запуске приложения – с помощью реализации службы InitializingBean :

public void afterPropertiesSet() {
    loadSubreddits();
}
private void loadSubreddits() {
    subreddits = new ArrayList();
    try {
        Resource resource = new ClassPathResource("subreddits.csv");
        Scanner scanner = new Scanner(resource.getFile());
        scanner.useDelimiter(",");
        while (scanner.hasNext()) {
            subreddits.add(scanner.next());
        }
        scanner.close();
    } catch (IOException e) {
        logger.error("error while loading subreddits", e);
    }
}

Теперь, когда все данные субреддита загружены в память, мы можем искать по субреддитам, не обращаясь к API Reddit :

public List searchSubreddit(String query) {
    return subreddits.stream().
      filter(sr -> sr.startsWith(query)).
      limit(9).
      collect(Collectors.toList());
}

API, предоставляющий предложения subreddit, конечно, остается прежним:

@RequestMapping(value = "/subredditAutoComplete")
@ResponseBody
public List subredditAutoComplete(@RequestParam("term") String term) {
    return service.searchSubreddit(term);
}

7. Метрики

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

7.1. Фильтр сервлетов

Здесь простой Метрический фильтр :

@Component
public class MetricFilter implements Filter {

    @Autowired
    private IMetricService metricService;

    @Override
    public void doFilter(
      ServletRequest request, ServletResponse response, FilterChain chain) 
      throws IOException, ServletException {
        HttpServletRequest httpRequest = ((HttpServletRequest) request);
        String req = httpRequest.getMethod() + " " + httpRequest.getRequestURI();

        chain.doFilter(request, response);

        int status = ((HttpServletResponse) response).getStatus();
        metricService.increaseCount(req, status);
    }
}

Нам также нужно добавить его в наш ServletInitializer :

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
    super.onStartup(servletContext);
    servletContext.addListener(new SessionListener());
    registerProxyFilter(servletContext, "oauth2ClientContextFilter");
    registerProxyFilter(servletContext, "springSecurityFilterChain");
    registerProxyFilter(servletContext, "metricFilter");
}

7.2. Метрическая служба

А вот наш Метрический сервис :

public interface IMetricService {
    void increaseCount(String request, int status);
    
    Map getFullMetric();
    Map getStatusMetric();
    
    Object[][] getGraphData();
}

7.3. Метрический контроллер

И вот основной контроллер, ответственный за предоставление этих метрик через HTTP:

@Controller
public class MetricController {
    
    @Autowired
    private IMetricService metricService;

    // 
    
    @RequestMapping(value = "/metric", method = RequestMethod.GET)
    @ResponseBody
    public Map getMetric() {
        return metricService.getFullMetric();
    }

    @RequestMapping(value = "/status-metric", method = RequestMethod.GET)
    @ResponseBody
    public Map getStatusMetric() {
        return metricService.getStatusMetric();
    }

    @RequestMapping(value = "/metric-graph-data", method = RequestMethod.GET)
    @ResponseBody
    public Object[][] getMetricGraphData() {
        Object[][] result = metricService.getGraphData();
        for (int i = 1; i < result[0].length; i++) {
            result[0][i] = result[0][i].toString();
        }
        return result;
    }
}

8. Заключение

Это тематическое исследование хорошо развивается. На самом деле приложение начиналось как простой учебник по работе с OAuth с помощью API Reddit; теперь оно превращается в полезный инструмент для опытных пользователей Reddit, особенно в отношении параметров планирования и повторной отправки.

Наконец, с тех пор, как я использую его, похоже, что мои собственные материалы в Reddit, как правило, набирают гораздо больше пара, так что это всегда приятно видеть.