diff --git a/backend/src/main/java/com/openisle/controller/MedalController.java b/backend/src/main/java/com/openisle/controller/MedalController.java new file mode 100644 index 000000000..086d3f844 --- /dev/null +++ b/backend/src/main/java/com/openisle/controller/MedalController.java @@ -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 getMedals(@RequestParam(value = "userId", required = false) Long userId) { + return medalService.getMedals(userId); + } +} diff --git a/backend/src/main/java/com/openisle/dto/CommentMedalDto.java b/backend/src/main/java/com/openisle/dto/CommentMedalDto.java new file mode 100644 index 000000000..045194f9b --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/CommentMedalDto.java @@ -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; +} diff --git a/backend/src/main/java/com/openisle/dto/MedalDto.java b/backend/src/main/java/com/openisle/dto/MedalDto.java new file mode 100644 index 000000000..757fe5c15 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/MedalDto.java @@ -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; +} diff --git a/backend/src/main/java/com/openisle/dto/PostMedalDto.java b/backend/src/main/java/com/openisle/dto/PostMedalDto.java new file mode 100644 index 000000000..9b538b207 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/PostMedalDto.java @@ -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; +} diff --git a/backend/src/main/java/com/openisle/dto/SeedUserMedalDto.java b/backend/src/main/java/com/openisle/dto/SeedUserMedalDto.java new file mode 100644 index 000000000..b1a961dbd --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/SeedUserMedalDto.java @@ -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; +} diff --git a/backend/src/main/java/com/openisle/model/MedalType.java b/backend/src/main/java/com/openisle/model/MedalType.java new file mode 100644 index 000000000..58ab4e22e --- /dev/null +++ b/backend/src/main/java/com/openisle/model/MedalType.java @@ -0,0 +1,7 @@ +package com.openisle.model; + +public enum MedalType { + COMMENT, + POST, + SEED +} diff --git a/backend/src/main/java/com/openisle/repository/CommentRepository.java b/backend/src/main/java/com/openisle/repository/CommentRepository.java index fa2418f58..de65699cd 100644 --- a/backend/src/main/java/com/openisle/repository/CommentRepository.java +++ b/backend/src/main/java/com/openisle/repository/CommentRepository.java @@ -30,4 +30,6 @@ public interface CommentRepository extends JpaRepository { @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); + } diff --git a/backend/src/main/java/com/openisle/repository/PostRepository.java b/backend/src/main/java/com/openisle/repository/PostRepository.java index bfe410dc2..1c238db75 100644 --- a/backend/src/main/java/com/openisle/repository/PostRepository.java +++ b/backend/src/main/java/com/openisle/repository/PostRepository.java @@ -93,4 +93,6 @@ public interface PostRepository extends JpaRepository { long countByCategory_Id(Long categoryId); long countDistinctByTags_Id(Long tagId); + + long countByAuthor_Id(Long userId); } diff --git a/backend/src/main/java/com/openisle/service/MedalService.java b/backend/src/main/java/com/openisle/service/MedalService.java new file mode 100644 index 000000000..b80f5965d --- /dev/null +++ b/backend/src/main/java/com/openisle/service/MedalService.java @@ -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 getMedals(Long userId) { + List 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; + } +} diff --git a/backend/src/test/java/com/openisle/controller/MedalControllerTest.java b/backend/src/test/java/com/openisle/controller/MedalControllerTest.java new file mode 100644 index 000000000..1dc054cab --- /dev/null +++ b/backend/src/test/java/com/openisle/controller/MedalControllerTest.java @@ -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)); + } +} diff --git a/backend/src/test/java/com/openisle/service/MedalServiceTest.java b/backend/src/test/java/com/openisle/service/MedalServiceTest.java new file mode 100644 index 000000000..297f1c47d --- /dev/null +++ b/backend/src/test/java/com/openisle/service/MedalServiceTest.java @@ -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 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 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()); + } +}