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

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

1. обзор

Давайте продолжим нашongoing Reddit web app case study с новым раундом улучшений с целью сделать приложение более удобным и простым в использовании.

2. Пагинация запланированных сообщений

Во-первых, давайте составим список запланированных публикацийwith pagination, чтобы упростить просмотр и понимание.

2.1. Операции с разбивкой на страницы

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

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

А вот наш метод контроллераgetScheduledPosts():

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. Показывать страницу входа не авторизованным пользователям

Когда пользователь обращается к корню,they should get different pages if they’re logged in or not.

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

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

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

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

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

So – that’s the first small yet highly useful feature we’ll add - новая опция, которая позволит нам удалить сообщение только в том случае, если к нему нет комментариев.

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

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

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

So – this is the third small feature we’re going to add - новая опция, позволяющая учитывать это соотношение голосов "за" и "против" при определении, нужно ли нам удалить публикацию или нет.

4.1. СущностьPost

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

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

Вот 3 поля:

  • minUpvoteRatio: минимальное количество голосов, которое пользователь хочет, чтобы его сообщение достигло - коэффициент поддержки показывает, сколько процентов от общего числа голосов было набрано [макс. = 100, мин. = 0]

  • keepIfHasComments: определите, хочет ли пользователь сохранить свое сообщение, если к нему есть комментарии, несмотря на то, что он не набрал требуемый балл.

  • deleteAfterLastAttempt: определите, хочет ли пользователь удалить сообщение после того, как последняя попытка закончится, не набрав требуемый балл.

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() -checking if the post failed to reach the predefined goal/score:

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;
}

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

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

Наконец, нам нужно изменитьcheckAndReSubmit(), чтобы установитьredditID успешно повторно отправленного сообщения на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);
        }
    }
}

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

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

  • getPostScores(): \ {оценка обратной записи, коэффициент положительных голосов, количество комментариев}

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

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



5. Отправить важные журналы по электронной почте

Затем мы реализуем быстрый, но очень полезный параметр в нашей конфигурации логбэка -emailing of important logs (ERROR level). Это, конечно, очень удобно, чтобы легко отслеживать ошибки на ранних этапах жизненного цикла приложения.

Сначала мы добавим несколько необходимых зависимостей в наш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. Кеш-субреддиты

Получается,auto-completing subreddits expensive. Каждый раз, когда пользователь начинает вводить subreddit при планировании публикации - нам нужно нажать на Reddit API, чтобы получить эти subreddits и показать пользователю некоторые предложения. Не идеально.

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

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);
    }
}

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

6.2. Subbreddit Autocomplete

Затем давайте удостоверимся, чтоthe subreddits are loaded into memory on application startup - если сервис реализует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);
    }
}

Теперь, когда все данные сабреддита загружены в память,we can search over the subreddits without hitting the Reddit API:

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

Конечно, API, предлагающий субреддитные предложения, остается прежним:

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

7. метрика

Наконец, мы интегрируем в приложение несколько простых показателей. Чтобы узнать больше о построении таких показателей,I wrote about them in some detail here.

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

Вот простойMetricFilter:

@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. Метрическая служба

А вот нашMetricService:

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 с помощью Reddit API; Теперь он превращается в полезный инструмент для опытных пользователей Reddit, особенно в отношении параметров планирования и повторной отправки.

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