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 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 ListsearchSubreddit(String query) { return subreddits.stream(). filter(sr -> sr.startsWith(query)). limit(9). collect(Collectors.toList()); }
API, предоставляющий предложения subreddit, конечно, остается прежним:
@RequestMapping(value = "/subredditAutoComplete") @ResponseBody public ListsubredditAutoComplete(@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, как правило, набирают гораздо больше пара, так что это всегда приятно видеть.