feat: implement medal feature

This commit is contained in:
Tim
2025-08-09 02:08:02 +08:00
parent 9c1cedd172
commit 987fe0d885
11 changed files with 269 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
package com.openisle.controller;
import com.openisle.dto.MedalDto;
import com.openisle.service.MedalService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/medals")
@RequiredArgsConstructor
public class MedalController {
private final MedalService medalService;
@GetMapping
public List<MedalDto> getMedals(@RequestParam(value = "userId", required = false) Long userId) {
return medalService.getMedals(userId);
}
}

View File

@@ -0,0 +1,11 @@
package com.openisle.dto;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class CommentMedalDto extends MedalDto {
private long currentCommentCount;
private long targetCommentCount;
}

View File

@@ -0,0 +1,13 @@
package com.openisle.dto;
import com.openisle.model.MedalType;
import lombok.Data;
@Data
public class MedalDto {
private String icon;
private String title;
private String description;
private MedalType type;
private boolean completed;
}

View File

@@ -0,0 +1,11 @@
package com.openisle.dto;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class PostMedalDto extends MedalDto {
private long currentPostCount;
private long targetPostCount;
}

View File

@@ -0,0 +1,11 @@
package com.openisle.dto;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class SeedUserMedalDto extends MedalDto {
private LocalDateTime registerDate;
}

View File

@@ -0,0 +1,7 @@
package com.openisle.model;
public enum MedalType {
COMMENT,
POST,
SEED
}

View File

@@ -30,4 +30,6 @@ public interface CommentRepository extends JpaRepository<Comment, Long> {
@org.springframework.data.jpa.repository.Query("SELECT COUNT(c) FROM Comment c WHERE c.post.id = :postId")
long countByPostId(@org.springframework.data.repository.query.Param("postId") Long postId);
long countByAuthor_Id(Long userId);
}

View File

@@ -93,4 +93,6 @@ public interface PostRepository extends JpaRepository<Post, Long> {
long countByCategory_Id(Long categoryId);
long countDistinctByTags_Id(Long tagId);
long countByAuthor_Id(Long userId);
}

View File

@@ -0,0 +1,84 @@
package com.openisle.service;
import com.openisle.dto.CommentMedalDto;
import com.openisle.dto.MedalDto;
import com.openisle.dto.PostMedalDto;
import com.openisle.dto.SeedUserMedalDto;
import com.openisle.model.MedalType;
import com.openisle.repository.CommentRepository;
import com.openisle.repository.PostRepository;
import com.openisle.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class MedalService {
private static final long COMMENT_TARGET = 100;
private static final long POST_TARGET = 100;
private static final LocalDateTime SEED_USER_DEADLINE = LocalDateTime.of(2025, 9, 16, 0, 0);
private final CommentRepository commentRepository;
private final PostRepository postRepository;
private final UserRepository userRepository;
public List<MedalDto> getMedals(Long userId) {
List<MedalDto> medals = new ArrayList<>();
CommentMedalDto commentMedal = new CommentMedalDto();
commentMedal.setIcon("comment.png");
commentMedal.setTitle("评论达人");
commentMedal.setDescription("评论超过100条");
commentMedal.setType(MedalType.COMMENT);
commentMedal.setTargetCommentCount(COMMENT_TARGET);
if (userId != null) {
long count = commentRepository.countByAuthor_Id(userId);
commentMedal.setCurrentCommentCount(count);
commentMedal.setCompleted(count >= COMMENT_TARGET);
} else {
commentMedal.setCurrentCommentCount(0);
commentMedal.setCompleted(false);
}
medals.add(commentMedal);
PostMedalDto postMedal = new PostMedalDto();
postMedal.setIcon("post.png");
postMedal.setTitle("发帖达人");
postMedal.setDescription("评论超过100条");
postMedal.setType(MedalType.POST);
postMedal.setTargetPostCount(POST_TARGET);
if (userId != null) {
long count = postRepository.countByAuthor_Id(userId);
postMedal.setCurrentPostCount(count);
postMedal.setCompleted(count >= POST_TARGET);
} else {
postMedal.setCurrentPostCount(0);
postMedal.setCompleted(false);
}
medals.add(postMedal);
SeedUserMedalDto seedUserMedal = new SeedUserMedalDto();
seedUserMedal.setIcon("seed.png");
seedUserMedal.setTitle("种子用户");
seedUserMedal.setDescription("2025.9.16前注册的用户");
seedUserMedal.setType(MedalType.SEED);
if (userId != null) {
userRepository.findById(userId).ifPresent(user -> {
seedUserMedal.setRegisterDate(user.getCreatedAt());
seedUserMedal.setCompleted(user.getCreatedAt().isBefore(SEED_USER_DEADLINE));
});
if (seedUserMedal.getRegisterDate() == null) {
seedUserMedal.setCompleted(false);
}
} else {
seedUserMedal.setCompleted(false);
}
medals.add(seedUserMedal);
return medals;
}
}

View File

@@ -0,0 +1,52 @@
package com.openisle.controller;
import com.openisle.dto.CommentMedalDto;
import com.openisle.model.MedalType;
import com.openisle.service.MedalService;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(MedalController.class)
@AutoConfigureMockMvc(addFilters = false)
class MedalControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private MedalService medalService;
@Test
void listMedals() throws Exception {
CommentMedalDto medal = new CommentMedalDto();
medal.setTitle("评论达人");
medal.setType(MedalType.COMMENT);
Mockito.when(medalService.getMedals(null)).thenReturn(List.of(medal));
mockMvc.perform(get("/api/medals"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].title").value("评论达人"));
}
@Test
void listMedalsWithUser() throws Exception {
CommentMedalDto medal = new CommentMedalDto();
medal.setCompleted(true);
medal.setType(MedalType.COMMENT);
Mockito.when(medalService.getMedals(1L)).thenReturn(List.of(medal));
mockMvc.perform(get("/api/medals").param("userId", "1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].completed").value(true));
}
}

View File

@@ -0,0 +1,53 @@
package com.openisle.service;
import com.openisle.dto.MedalDto;
import com.openisle.model.MedalType;
import com.openisle.model.User;
import com.openisle.repository.CommentRepository;
import com.openisle.repository.PostRepository;
import com.openisle.repository.UserRepository;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class MedalServiceTest {
@Test
void getMedalsWithoutUser() {
CommentRepository commentRepo = mock(CommentRepository.class);
PostRepository postRepo = mock(PostRepository.class);
UserRepository userRepo = mock(UserRepository.class);
MedalService service = new MedalService(commentRepo, postRepo, userRepo);
List<MedalDto> medals = service.getMedals(null);
assertFalse(medals.get(0).isCompleted());
assertFalse(medals.get(1).isCompleted());
assertFalse(medals.get(2).isCompleted());
}
@Test
void getMedalsWithUser() {
CommentRepository commentRepo = mock(CommentRepository.class);
PostRepository postRepo = mock(PostRepository.class);
UserRepository userRepo = mock(UserRepository.class);
when(commentRepo.countByAuthor_Id(1L)).thenReturn(120L);
when(postRepo.countByAuthor_Id(1L)).thenReturn(80L);
User user = new User();
user.setId(1L);
user.setCreatedAt(LocalDateTime.of(2025, 9, 15, 0, 0));
when(userRepo.findById(1L)).thenReturn(Optional.of(user));
MedalService service = new MedalService(commentRepo, postRepo, userRepo);
List<MedalDto> medals = service.getMedals(1L);
assertTrue(medals.stream().filter(m -> m.getType() == MedalType.COMMENT).findFirst().orElseThrow().isCompleted());
assertFalse(medals.stream().filter(m -> m.getType() == MedalType.POST).findFirst().orElseThrow().isCompleted());
assertTrue(medals.stream().filter(m -> m.getType() == MedalType.SEED).findFirst().orElseThrow().isCompleted());
}
}