Zweite Runde der Verbesserungen an der Reddit-Anwendung

Zweite Runde der Verbesserungen an der Reddit-Anwendung

1. Überblick

Setzen wir unsereongoing Reddit web app case study mit einer neuen Runde von Verbesserungen fort, mit dem Ziel, die Anwendung benutzerfreundlicher und benutzerfreundlicher zu gestalten.

2. Geplante Posts Paginierung

Lassen Sie uns zunächst die geplanten Postswith pagination auflisten, damit das Ganze leichter zu sehen und zu verstehen ist.

2.1. Die paginierten Operationen

Wir werden Spring Data verwenden, um die Operation zu generieren, die wir benötigen, und diePageable-Schnittstelle gut nutzen, um die geplanten Beiträge des Benutzers abzurufen:

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

Und hier ist unsere Controller-MethodegetScheduledPosts():

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. Paginierte Beiträge anzeigen

Jetzt implementieren wir eine einfache Paginierungssteuerung im Frontend:

Post title

Und so laden wir die Seiten mit einfachem 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+'');
        });
    });
}

Während wir fortfahren, wird diese manuelle Tabelle schnell durch ein ausgereifteres Tabellen-Plugin ersetzt, aber im Moment funktioniert dies einwandfrei.

3. Zeigen Sie die Anmeldeseite nicht angemeldeten Benutzern an

Wenn ein Benutzer auf das Stammverzeichnis zugreift,they should get different pages if they’re logged in or not.

Wenn der Benutzer angemeldet ist, sollte seine Homepage / sein Dashboard angezeigt werden. Wenn sie nicht angemeldet sind, sollte die Anmeldeseite angezeigt werden:

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

4. Erweiterte Optionen für die erneute Übermittlung

Das Entfernen und erneute Einreichen von Beiträgen in Reddit ist eine nützliche und äußerst effektive Funktion. Wir möchten jedoch vorsichtig damit sein undhave full controlist vorbei, wann wir sollten und wann wir es nicht tun sollten.

Zum Beispiel: Möglicherweise möchten wir einen Beitrag nicht entfernen, wenn er bereits Kommentare enthält. Letztendlich sind Kommentare eine Verpflichtung und wir möchten die Plattform und die Leute respektieren, die zu dem Beitrag Stellung nehmen.

So – that’s the first small yet highly useful feature we’ll add - Eine neue Option, mit der wir einen Beitrag nur entfernen können, wenn er keine Kommentare enthält.

Eine weitere sehr interessante Frage ist: Wenn der Beitrag so oft erneut eingereicht wird, aber immer noch nicht die erforderliche Traktion erhält, lassen wir ihn nach dem letzten Versuch eingeschaltet oder nicht. Nun, wie bei allen interessanten Fragen lautet die Antwort hier - "es kommt darauf an". Wenn es sich um einen normalen Beitrag handelt, nennen wir ihn möglicherweise nur einen Tag und lassen ihn offen. Wenn es sich jedoch um einen sehr wichtigen Beitrag handelt und wir wirklich wirklich sicherstellen möchten, dass er eine gewisse Traktion erhält, können wir ihn am Ende löschen.

Dies ist also die zweite kleine, aber sehr praktische Funktion, die wir hier erstellen werden.

Schließlich - was ist mit kontroversen Beiträgen? Ein Beitrag kann 2 Stimmen bei reddit haben, weil er dort positive Stimmen hat oder weil er 100 positive und 98 negative Stimmen hat. Die erste Option bedeutet, dass es keine Traktion bekommt, während die zweite bedeutet, dass es viel Traktion bekommt und dass die Abstimmung aufgeteilt wird.

So – this is the third small feature we’re going to add - eine neue Option, um dieses Verhältnis von Upvote zu Downvote zu berücksichtigen, wenn festgestellt wird, ob der Beitrag entfernt werden muss oder nicht.

4.1. DiePost-Entität

Zuerst müssen wir die EntitätPoständern:

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

Hier sind die 3 Felder:

  • minUpvoteRatio: Das minimale Upvote-Verhältnis, das der Benutzer mit seinem Beitrag erreichen möchte - das Upvote-Verhältnis gibt an, wie viel Prozent der Gesamtstimmen ara upvotes sind [max = 100, min = 0]

  • keepIfHasComments: Bestimmen Sie, ob der Benutzer seinen Beitrag behalten möchte, wenn er Kommentare enthält, obwohl er die erforderliche Punktzahl nicht erreicht hat.

  • deleteAfterLastAttempt: Bestimmen Sie, ob der Benutzer den Beitrag nach dem Ende des letzten Versuchs löschen möchte, ohne die erforderliche Punktzahl zu erreichen.

4.2. Der Scheduler

Lassen Sie uns nun diese interessanten neuen Optionen in den Scheduler integrieren:

@Scheduled(fixedRate = 3 * 60 * 1000)
public void checkAndDeleteAll() {
    List submitted =
      postReopsitory.findByRedditIDNotNullAndNoOfAttemptsAndDeleteAfterLastAttemptTrue(0);

    for (Post post : submitted) {
        checkAndDelete(post);
    }
}

Zum interessanteren Teil - der tatsächlichen Logik voncheckAndDelete():

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

Und hier ist die Implementierung vondidPostGoalFail()-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()));
}

Wir müssen auch die Logik ändern, die diePost-Informationen von Reddit abruft, um sicherzustellen, dass wir mehr Daten erfassen:

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

Wir verwenden ein einfaches Wertobjekt, um die Ergebnisse darzustellen, während wir sie aus der Reddit-API extrahieren:

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

Schließlich müssen wircheckAndReSubmit() ändern, um dieredditID des erfolgreich erneut eingereichten Posts aufnull zu setzen:

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

Beachten Sie, dass:

  • checkAndDeleteAll(): Läuft alle 3 Minuten durch, um festzustellen, ob Beiträge ihre Versuche verbraucht haben und gelöscht werden können

  • getPostScores(): \ {Punktzahl, Upvote-Verhältnis, Anzahl der Kommentare}

4.3. Ändern Sie die Zeitplanseite

Wir müssen die neuen Änderungen zu unserenschedulePostForm.html hinzufügen:



5. Wichtige Protokolle per E-Mail senden

Als Nächstes implementieren wir eine schnelle, aber äußerst nützliche Einstellung in unserer Logback-Konfiguration -emailing of important logs (ERROR level). Dies ist natürlich sehr praktisch, um Fehler frühzeitig im Lebenszyklus einer Anwendung zu verfolgen.

Zunächst fügen wir unserenpom.xml einige erforderliche Abhängigkeiten hinzu:


    javax.activation
    activation
    1.1.1


    javax.mail
    mail
    1.4.1

Dann fügen wir unserenlogback.xml einSMTPAppender hinzu:



    
        
            ERROR
            ACCEPT
            DENY
        

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

    
        
        
    

Und das war's auch schon - jetzt sendet die bereitgestellte Anwendung jedes Problem per E-Mail, sobald es passiert.

6. Subreddits zwischenspeichern

Es stellt sich heraus,auto-completing subreddits expensive. Jedes Mal, wenn ein Benutzer beim Planen eines Posts mit der Eingabe eines Subreddits beginnt, müssen wir die Reddit-API aufrufen, um diese Subreddits abzurufen und dem Benutzer einige Vorschläge zu zeigen. Nicht ideal.

Anstatt die Reddit-API aufzurufen, werden die beliebten Subreddits einfach zwischengespeichert und zur automatischen Vervollständigung verwendet.

6.1. Subreddits abrufen

Lassen Sie uns zunächst die beliebtesten Subreddits abrufen und in einer einfachen Datei speichern:

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

Ist das eine ausgereifte Implementierung? No. Brauchen wir noch mehr Nein, das tun wir nicht. Wir müssen weitermachen.

6.2. Subbreddit Autocomplete

Stellen wir als Nächstes sicher, dassthe subreddits are loaded into memory on application startup - indem der DienstInitializingBean implementiert:

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

Nachdem alle Subreddit-Daten in den Speicher geladen wurden, werdenwe 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());
}

Die API, die die Subreddit-Vorschläge enthält, bleibt natürlich dieselbe:

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

7. Metriken

Schließlich werden wir einige einfache Metriken in die Anwendung integrieren. Für viel mehr zum Aufbau dieser Art von Metriken,I wrote about them in some detail here.

7.1. Servlet-Filter

Hier die einfachenMetricFilter:

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

Wir müssen es auch in unserenServletInitializer hinzufügen:

@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. Metrischer Service

Und hier sind unsereMetricService:

public interface IMetricService {
    void increaseCount(String request, int status);

    Map getFullMetric();
    Map getStatusMetric();

    Object[][] getGraphData();
}

7.3. Metric Controller

Und sie ist der grundlegende Controller, der für die Bereitstellung dieser Metriken über HTTP verantwortlich ist:

@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. Fazit

Diese Fallstudie wächst gut. Die App wurde als einfaches Tutorial zum Ausführen von OAuth mit der Reddit-API gestartet. Jetzt entwickelt es sich zu einem nützlichen Tool für den Reddit-Power-User - insbesondere in Bezug auf die Planung und erneute Übermittlung von Optionen.

Seit ich es benutze, sieht es so aus, als würden meine eigenen Beiträge bei Reddit im Allgemeinen viel mehr Dampf aufnehmen. Das ist also immer gut zu sehen.