diff --git a/backend/src/main/java/com/openisle/controller/MedalController.java b/backend/src/main/java/com/openisle/controller/MedalController.java index 086d3f844..b710f7557 100644 --- a/backend/src/main/java/com/openisle/controller/MedalController.java +++ b/backend/src/main/java/com/openisle/controller/MedalController.java @@ -1,12 +1,12 @@ package com.openisle.controller; import com.openisle.dto.MedalDto; +import com.openisle.dto.MedalSelectRequest; 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 org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; import java.util.List; @@ -20,4 +20,14 @@ public class MedalController { public List getMedals(@RequestParam(value = "userId", required = false) Long userId) { return medalService.getMedals(userId); } + + @PostMapping("/select") + public ResponseEntity selectMedal(@RequestBody MedalSelectRequest req, Authentication auth) { + try { + medalService.selectMedal(auth.getName(), req.getType()); + return ResponseEntity.ok().build(); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().build(); + } + } } diff --git a/backend/src/main/java/com/openisle/dto/AuthorDto.java b/backend/src/main/java/com/openisle/dto/AuthorDto.java index 9892df611..4f0bc4887 100644 --- a/backend/src/main/java/com/openisle/dto/AuthorDto.java +++ b/backend/src/main/java/com/openisle/dto/AuthorDto.java @@ -1,6 +1,7 @@ package com.openisle.dto; import lombok.Data; +import com.openisle.model.MedalType; /** * DTO representing a post or comment author. @@ -10,5 +11,6 @@ public class AuthorDto { private Long id; private String username; private String avatar; + private MedalType displayMedal; } diff --git a/backend/src/main/java/com/openisle/dto/MedalDto.java b/backend/src/main/java/com/openisle/dto/MedalDto.java index 757fe5c15..5d6bc8f9a 100644 --- a/backend/src/main/java/com/openisle/dto/MedalDto.java +++ b/backend/src/main/java/com/openisle/dto/MedalDto.java @@ -10,4 +10,5 @@ public class MedalDto { private String description; private MedalType type; private boolean completed; + private boolean selected; } diff --git a/backend/src/main/java/com/openisle/dto/MedalSelectRequest.java b/backend/src/main/java/com/openisle/dto/MedalSelectRequest.java new file mode 100644 index 000000000..0d1f94d1d --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/MedalSelectRequest.java @@ -0,0 +1,9 @@ +package com.openisle.dto; + +import com.openisle.model.MedalType; +import lombok.Data; + +@Data +public class MedalSelectRequest { + private MedalType type; +} diff --git a/backend/src/main/java/com/openisle/mapper/UserMapper.java b/backend/src/main/java/com/openisle/mapper/UserMapper.java index 0244b151e..6e9cfef0e 100644 --- a/backend/src/main/java/com/openisle/mapper/UserMapper.java +++ b/backend/src/main/java/com/openisle/mapper/UserMapper.java @@ -31,6 +31,7 @@ public class UserMapper { dto.setId(user.getId()); dto.setUsername(user.getUsername()); dto.setAvatar(user.getAvatar()); + dto.setDisplayMedal(user.getDisplayMedal()); return dto; } diff --git a/backend/src/main/java/com/openisle/model/User.java b/backend/src/main/java/com/openisle/model/User.java index 8a17dfa85..43bcbe268 100644 --- a/backend/src/main/java/com/openisle/model/User.java +++ b/backend/src/main/java/com/openisle/model/User.java @@ -59,6 +59,9 @@ public class User { @Column(nullable = false) private Role role = Role.USER; + @Enumerated(EnumType.STRING) + private MedalType displayMedal; + @CreationTimestamp @Column(nullable = false, updatable = false, columnDefinition = "DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6)") diff --git a/backend/src/main/java/com/openisle/service/MedalService.java b/backend/src/main/java/com/openisle/service/MedalService.java index daba7c97a..f40ceeb00 100644 --- a/backend/src/main/java/com/openisle/service/MedalService.java +++ b/backend/src/main/java/com/openisle/service/MedalService.java @@ -5,6 +5,7 @@ import com.openisle.dto.MedalDto; import com.openisle.dto.PostMedalDto; import com.openisle.dto.SeedUserMedalDto; 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; @@ -28,6 +29,11 @@ public class MedalService { public List getMedals(Long userId) { List medals = new ArrayList<>(); + User user = null; + if (userId != null) { + user = userRepository.findById(userId).orElse(null); + } + MedalType selected = user != null ? user.getDisplayMedal() : null; CommentMedalDto commentMedal = new CommentMedalDto(); commentMedal.setIcon("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/achi_comment.png"); @@ -35,7 +41,7 @@ public class MedalService { commentMedal.setDescription("评论超过100条"); commentMedal.setType(MedalType.COMMENT); commentMedal.setTargetCommentCount(COMMENT_TARGET); - if (userId != null) { + if (user != null) { long count = commentRepository.countByAuthor_Id(userId); commentMedal.setCurrentCommentCount(count); commentMedal.setCompleted(count >= COMMENT_TARGET); @@ -43,6 +49,7 @@ public class MedalService { commentMedal.setCurrentCommentCount(0); commentMedal.setCompleted(false); } + commentMedal.setSelected(selected == MedalType.COMMENT); medals.add(commentMedal); PostMedalDto postMedal = new PostMedalDto(); @@ -51,7 +58,7 @@ public class MedalService { postMedal.setDescription("发帖超过100条"); postMedal.setType(MedalType.POST); postMedal.setTargetPostCount(POST_TARGET); - if (userId != null) { + if (user != null) { long count = postRepository.countByAuthor_Id(userId); postMedal.setCurrentPostCount(count); postMedal.setCompleted(count >= POST_TARGET); @@ -59,6 +66,7 @@ public class MedalService { postMedal.setCurrentPostCount(0); postMedal.setCompleted(false); } + postMedal.setSelected(selected == MedalType.POST); medals.add(postMedal); SeedUserMedalDto seedUserMedal = new SeedUserMedalDto(); @@ -66,19 +74,29 @@ public class MedalService { 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); - } + if (user != null) { + seedUserMedal.setRegisterDate(user.getCreatedAt()); + seedUserMedal.setCompleted(user.getCreatedAt().isBefore(SEED_USER_DEADLINE)); } else { seedUserMedal.setCompleted(false); } + seedUserMedal.setSelected(selected == MedalType.SEED); medals.add(seedUserMedal); return medals; } + + public void selectMedal(String username, MedalType type) { + User user = userRepository.findByUsername(username).orElseThrow(); + boolean completed = getMedals(user.getId()).stream() + .filter(m -> m.getType() == type) + .findFirst() + .map(MedalDto::isCompleted) + .orElse(false); + if (!completed) { + throw new IllegalArgumentException("Medal not completed"); + } + user.setDisplayMedal(type); + userRepository.save(user); + } } diff --git a/backend/src/test/java/com/openisle/controller/MedalControllerTest.java b/backend/src/test/java/com/openisle/controller/MedalControllerTest.java index 1dc054cab..1853f92b8 100644 --- a/backend/src/test/java/com/openisle/controller/MedalControllerTest.java +++ b/backend/src/test/java/com/openisle/controller/MedalControllerTest.java @@ -9,11 +9,13 @@ 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.http.MediaType; 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.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -49,4 +51,24 @@ class MedalControllerTest { .andExpect(status().isOk()) .andExpect(jsonPath("$[0].completed").value(true)); } + + @Test + void selectMedal() throws Exception { + mockMvc.perform(post("/api/medals/select") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"type\":\"COMMENT\"}") + .principal(() -> "user")) + .andExpect(status().isOk()); + } + + @Test + void selectMedalBadRequest() throws Exception { + Mockito.doThrow(new IllegalArgumentException()).when(medalService) + .selectMedal("user", MedalType.COMMENT); + mockMvc.perform(post("/api/medals/select") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"type\":\"COMMENT\"}") + .principal(() -> "user")) + .andExpect(status().isBadRequest()); + } } diff --git a/backend/src/test/java/com/openisle/service/MedalServiceTest.java b/backend/src/test/java/com/openisle/service/MedalServiceTest.java index 297f1c47d..bcc8659c3 100644 --- a/backend/src/test/java/com/openisle/service/MedalServiceTest.java +++ b/backend/src/test/java/com/openisle/service/MedalServiceTest.java @@ -41,13 +41,55 @@ class MedalServiceTest { User user = new User(); user.setId(1L); user.setCreatedAt(LocalDateTime.of(2025, 9, 15, 0, 0)); + user.setDisplayMedal(MedalType.COMMENT); when(userRepo.findById(1L)).thenReturn(Optional.of(user)); + when(userRepo.findByUsername("user")).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()); + assertTrue(medals.stream().filter(m -> m.getType() == MedalType.COMMENT).findFirst().orElseThrow().isSelected()); assertFalse(medals.stream().filter(m -> m.getType() == MedalType.POST).findFirst().orElseThrow().isCompleted()); + assertFalse(medals.stream().filter(m -> m.getType() == MedalType.POST).findFirst().orElseThrow().isSelected()); assertTrue(medals.stream().filter(m -> m.getType() == MedalType.SEED).findFirst().orElseThrow().isCompleted()); + assertFalse(medals.stream().filter(m -> m.getType() == MedalType.SEED).findFirst().orElseThrow().isSelected()); + } + + @Test + void selectMedal() { + 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(0L); + User user = new User(); + user.setId(1L); + user.setCreatedAt(LocalDateTime.of(2025, 9, 15, 0, 0)); + when(userRepo.findByUsername("user")).thenReturn(Optional.of(user)); + when(userRepo.findById(1L)).thenReturn(Optional.of(user)); + + MedalService service = new MedalService(commentRepo, postRepo, userRepo); + service.selectMedal("user", MedalType.COMMENT); + assertEquals(MedalType.COMMENT, user.getDisplayMedal()); + } + + @Test + void selectMedalNotCompleted() { + CommentRepository commentRepo = mock(CommentRepository.class); + PostRepository postRepo = mock(PostRepository.class); + UserRepository userRepo = mock(UserRepository.class); + + when(commentRepo.countByAuthor_Id(1L)).thenReturn(10L); + when(postRepo.countByAuthor_Id(1L)).thenReturn(0L); + User user = new User(); + user.setId(1L); + user.setCreatedAt(LocalDateTime.of(2025, 9, 15, 0, 0)); + when(userRepo.findByUsername("user")).thenReturn(Optional.of(user)); + when(userRepo.findById(1L)).thenReturn(Optional.of(user)); + + MedalService service = new MedalService(commentRepo, postRepo, userRepo); + assertThrows(IllegalArgumentException.class, () -> service.selectMedal("user", MedalType.COMMENT)); } } diff --git a/frontend_nuxt/components/AchievementList.vue b/frontend_nuxt/components/AchievementList.vue index bb687a123..aefc7bbdf 100644 --- a/frontend_nuxt/components/AchievementList.vue +++ b/frontend_nuxt/components/AchievementList.vue @@ -3,14 +3,15 @@
-
展示
+
展示
{{ medal.title }}
{{ medal.description }} @@ -27,12 +28,18 @@