Перейти к основному содержимому

Сохраняйте историю отправки сообщений Reddit

· 6 мин. чтения

1. Обзор

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

2. Улучшение сущности сообщения

Во-первых, давайте начнем с замены старого состояния String в сущности Post гораздо более полным списком ответов на отправку, отслеживая гораздо больше информации:

public class Post {
...
@OneToMany(fetch = FetchType.EAGER, mappedBy = "post")
private List<SubmissionResponse> submissionsResponse;
}

Далее, давайте посмотрим, что мы на самом деле отслеживаем в этом новом объекте ответа на отправку:

@Entity
public class SubmissionResponse implements IEntity {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

private int attemptNumber;

private String content;

private Date submissionDate;

private Date scoreCheckDate;

@JsonIgnore
@ManyToOne
@JoinColumn(name = "post_id", nullable = false)
private Post post;

public SubmissionResponse(int attemptNumber, String content, Post post) {
super();
this.attemptNumber = attemptNumber;
this.content = content;
this.submissionDate = new Date();
this.post = post;
}

@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Attempt No ").append(attemptNumber).append(" : ").append(content);
return builder.toString();
}
}

Обратите внимание, что каждая использованная попытка отправки имеет SubmissionResponse , так что:

  • tryNumber : номер этой попытки
  • content : подробный ответ на эту попытку
  • submitDate : дата отправки этой попытки
  • scoreCheckDate : дата, когда мы проверили оценку сообщения Reddit в этой попытке .

А вот простой репозиторий Spring Data JPA:

public interface SubmissionResponseRepository extends JpaRepository<SubmissionResponse, Long> {

SubmissionResponse findOneByPostAndAttemptNumber(Post post, int attemptNumber);
}

3. Служба планирования

Теперь нам нужно начать изменять сервисный слой, чтобы отслеживать эту дополнительную информацию.

Сначала мы убедимся, что у нас есть хорошо отформатированные причины успеха или неудачи, по которым публикация считается успешной или неудачной:

private final static String SCORE_TEMPLATE = "score %d %s minimum score %d";
private final static String TOTAL_VOTES_TEMPLATE = "total votes %d %s minimum total votes %d";

protected String getFailReason(Post post, PostScores postScores) {
StringBuilder builder = new StringBuilder();
builder.append("Failed because ");
builder.append(String.format(
SCORE_TEMPLATE, postScores.getScore(), "<", post.getMinScoreRequired()));

if (post.getMinTotalVotes() > 0) {
builder.append(" and ");
builder.append(String.format(TOTAL_VOTES_TEMPLATE,
postScores.getTotalVotes(), "<", post.getMinTotalVotes()));
}
if (post.isKeepIfHasComments()) {
builder.append(" and has no comments");
}
return builder.toString();
}

protected String getSuccessReason(Post post, PostScores postScores) {
StringBuilder builder = new StringBuilder();
if (postScores.getScore() >= post.getMinScoreRequired()) {
builder.append("Succeed because ");
builder.append(String.format(SCORE_TEMPLATE,
postScores.getScore(), ">=", post.getMinScoreRequired()));
return builder.toString();
}
if (
(post.getMinTotalVotes() > 0) &&
(postScores.getTotalVotes() >= post.getMinTotalVotes())
) {
builder.append("Succeed because ");
builder.append(String.format(TOTAL_VOTES_TEMPLATE,
postScores.getTotalVotes(), ">=", post.getMinTotalVotes()));
return builder.toString();
}
return "Succeed because has comments";
}

Теперь мы улучшим старую логику и будем отслеживать эту дополнительную историческую информацию :

private void submitPost(...) {
...
if (errorNode == null) {
post.setSubmissionsResponse(addAttemptResponse(post, "Submitted to Reddit"));
...
} else {
post.setSubmissionsResponse(addAttemptResponse(post, errorNode.toString()));
...
}
}
private void checkAndReSubmit(Post post) {
if (didIntervalPass(...)) {
PostScores postScores = getPostScores(post);
if (didPostGoalFail(post, postScores)) {
...
resetPost(post, getFailReason(post, postScores));
} else {
...
updateLastAttemptResponse(
post, "Post reached target score successfully " +
getSuccessReason(post, postScores));
}
}
}
private void checkAndDeleteInternal(Post post) {
if (didIntervalPass(...)) {
PostScores postScores = getPostScores(post);
if (didPostGoalFail(post, postScores)) {
updateLastAttemptResponse(post,
"Deleted from reddit, consumed all attempts without reaching score " +
getFailReason(post, postScores));
...
} else {
updateLastAttemptResponse(post,
"Post reached target score successfully " +
getSuccessReason(post, postScores));
...
}
}
}
private void resetPost(Post post, String failReason) {
...
updateLastAttemptResponse(post, "Deleted from Reddit, to be resubmitted " + failReason);
...
}

Обратите внимание, что на самом деле делают методы более низкого уровня:

  • addAttemptResponse() : создает новую запись SubmissionResponse и добавляет ее в публикацию (вызывается при каждой попытке отправки)
  • updateLastAttemptResponse() : обновить ответ последней попытки (вызывается при проверке оценки поста)

4. Запланированная публикация DTO

Затем мы изменим DTO, чтобы эта новая информация была доступна клиенту:

public class ScheduledPostDto {
...

private String status;

private List<SubmissionResponseDto> detailedStatus;
}

А вот простой SubmissionResponseDto :

public class SubmissionResponseDto {

private int attemptNumber;

private String content;

private String localSubmissionDate;

private String localScoreCheckDate;
}

Мы также изменим метод преобразования в нашем ScheduledPostRestController :

private ScheduledPostDto convertToDto(Post post) {
...
List<SubmissionResponse> response = post.getSubmissionsResponse();
if ((response != null) && (response.size() > 0)) {
postDto.setStatus(response.get(response.size() - 1).toString().substring(0, 30));
List<SubmissionResponseDto> responsedto =
post.getSubmissionsResponse().stream().
map(res -> generateResponseDto(res)).collect(Collectors.toList());
postDto.setDetailedStatus(responsedto);
} else {
postDto.setStatus("Not sent yet");
postDto.setDetailedStatus(Collections.emptyList());
}
return postDto;
}

private SubmissionResponseDto generateResponseDto(SubmissionResponse responseEntity) {
SubmissionResponseDto dto = modelMapper.map(responseEntity, SubmissionResponseDto.class);
String timezone = userService.getCurrentUser().getPreference().getTimezone();
dto.setLocalSubmissionDate(responseEntity.getSubmissionDate(), timezone);
if (responseEntity.getScoreCheckDate() != null) {
dto.setLocalScoreCheckDate(responseEntity.getScoreCheckDate(), timezone);
}
return dto;
}

5. Передняя часть

Далее мы изменим наш внешний интерфейс schedulePosts.jsp , чтобы он обрабатывал наш новый ответ:

<div class="modal">
<h4 class="modal-title">Detailed Status</h4>
<table id="res"></table>
</div>

<script >
var loadedData = [];
var detailedResTable = $('#res').DataTable( {
"searching":false,
"paging": false,
columns: [
{ title: "Attempt Number", data: "attemptNumber" },
{ title: "Detailed Status", data: "content" },
{ title: "Attempt Submitted At", data: "localSubmissionDate" },
{ title: "Attempt Score Checked At", data: "localScoreCheckDate" }
]
} );

$(document).ready(function() {
$('#myposts').dataTable( {
...
"columnDefs": [
{ "targets": 2, "data": "status",
"render": function ( data, type, full, meta ) {
return data +
' <a href="#" onclick="showDetailedStatus('+meta.row+' )">More Details</a>';
}
},
....
],
...
});
});

function showDetailedStatus(row){
detailedResTable.clear().rows.add(loadedData[row].detailedStatus).draw();
$('.modal').modal();
}

</script>

6. Тесты

Наконец, мы выполним простой модульный тест для наших новых методов:

Сначала мы протестируем реализацию getSuccessReason() :

@Test
public void whenHasEnoughScore_thenSucceed() {
Post post = new Post();
post.setMinScoreRequired(5);
PostScores postScores = new PostScores(6, 10, 1);

assertTrue(getSuccessReason(post, postScores).contains("Succeed because score"));
}

@Test
public void whenHasEnoughTotalVotes_thenSucceed() {
Post post = new Post();
post.setMinScoreRequired(5);
post.setMinTotalVotes(8);
PostScores postScores = new PostScores(2, 10, 1);

assertTrue(getSuccessReason(post, postScores).contains("Succeed because total votes"));
}

@Test
public void givenKeepPostIfHasComments_whenHasComments_thenSucceed() {
Post post = new Post();
post.setMinScoreRequired(5);
post.setKeepIfHasComments(true);
final PostScores postScores = new PostScores(2, 10, 1);

assertTrue(getSuccessReason(post, postScores).contains("Succeed because has comments"));
}

Далее мы протестируем реализацию getFailReason() :

@Test
public void whenNotEnoughScore_thenFail() {
Post post = new Post();
post.setMinScoreRequired(5);
PostScores postScores = new PostScores(2, 10, 1);

assertTrue(getFailReason(post, postScores).contains("Failed because score"));
}

@Test
public void whenNotEnoughTotalVotes_thenFail() {
Post post = new Post();
post.setMinScoreRequired(5);
post.setMinTotalVotes(15);
PostScores postScores = new PostScores(2, 10, 1);

String reason = getFailReason(post, postScores);
assertTrue(reason.contains("Failed because score"));
assertTrue(reason.contains("and total votes"));
}

@Test
public void givenKeepPostIfHasComments_whenNotHasComments_thenFail() {
Post post = new Post();
post.setMinScoreRequired(5);
post.setKeepIfHasComments(true);
final PostScores postScores = new PostScores(2, 10, 0);

String reason = getFailReason(post, postScores);
assertTrue(reason.contains("Failed because score"));
assertTrue(reason.contains("and has no comments"));
}

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

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