Browse Source

查询岗位评价接口新增筛选参数

backup/master-local-20261012
pwklppg 2 months ago
parent
commit
0f194cebef
  1. 13
      mydreamjob-server/src/main/java/com/mydreamjob/review/JobReviewController.java
  2. 23
      mydreamjob-server/src/main/java/com/mydreamjob/review/JobReviewRepository.java
  3. 26
      mydreamjob-server/src/main/java/com/mydreamjob/review/JobReviewService.java
  4. 2
      mydreamjob-server/src/test/java/com/mydreamjob/review/JobReviewControllerTest.java
  5. 57
      mydreamjob-server/src/test/java/com/mydreamjob/review/JobReviewServiceTest.java

13
mydreamjob-server/src/main/java/com/mydreamjob/review/JobReviewController.java

@ -11,11 +11,13 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -40,14 +42,21 @@ public class JobReviewController {
return ResponseEntity.ok(ApiResponse.ok(view, getRequestId(request))); return ResponseEntity.ok(ApiResponse.ok(view, getRequestId(request)));
} }
@Operation(summary = "查询岗位评价列表", description = "公开访问;仅返回 PUBLISHED 评价,分页;每条含八维评分") @Operation(summary = "查询岗位评价列表", description = "公开访问;仅返回 PUBLISHED 评价,分页;每条含八维评分;支持评分档/时间区间/未回复筛选")
@GetMapping("/jobs/{jobId}/reviews") @GetMapping("/jobs/{jobId}/reviews")
public ResponseEntity<ApiResponse<Map<String, Object>>> list( public ResponseEntity<ApiResponse<Map<String, Object>>> list(
@PathVariable Long jobId, @PathVariable Long jobId,
@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size, @RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String ratingType,
@RequestParam(defaultValue = "false") boolean unreplied,
@RequestParam(required = false)
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime createdFrom,
@RequestParam(required = false)
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime createdTo,
HttpServletRequest request) { HttpServletRequest request) {
Page<Map<String, Object>> result = reviewService.listReviews(jobId, page, size); Page<Map<String, Object>> result = reviewService.listReviews(
jobId, ratingType, createdFrom, createdTo, unreplied, page, size);
Map<String, Object> data = new HashMap<>(); Map<String, Object> data = new HashMap<>();
data.put("items", result.getContent()); data.put("items", result.getContent());
data.put("total", result.getTotalElements()); data.put("total", result.getTotalElements());

23
mydreamjob-server/src/main/java/com/mydreamjob/review/JobReviewRepository.java

@ -41,6 +41,29 @@ public interface JobReviewRepository extends JpaRepository<JobReview, Long> {
long countByUaHashAndCreatedAtAfter(String uaHash, LocalDateTime after); long countByUaHashAndCreatedAtAfter(String uaHash, LocalDateTime after);
/** 公开列表(含筛选):PUBLISHED 且审核通过;支持评分档/时间范围/未回复筛选 */
@Query("SELECT r FROM JobReview r"
+ " WHERE r.jobId = :jobId AND r.status = :status"
+ " AND r.moderationStatus IN :moderationStatuses"
+ " AND (:ratingType IS NULL OR"
+ " (:ratingType = 'GOOD' AND r.rating = 5)"
+ " OR (:ratingType = 'MEDIUM' AND r.rating IN (3,4))"
+ " OR (:ratingType = 'BAD' AND r.rating IN (1,2)))"
+ " AND (:createdFrom IS NULL OR r.createdAt >= :createdFrom)"
+ " AND (:createdTo IS NULL OR r.createdAt <= :createdTo)"
+ " AND (:unreplied = false OR NOT EXISTS"
+ " (SELECT 1 FROM CompanyReviewReply rr"
+ " WHERE rr.reviewId = r.id AND rr.moderationStatus = 'APPROVED'))"
+ " ORDER BY r.createdAt DESC")
Page<JobReview> findPublicReviews(@Param("jobId") Long jobId,
@Param("status") String status,
@Param("moderationStatuses") Collection<String> moderationStatuses,
@Param("ratingType") String ratingType,
@Param("createdFrom") LocalDateTime createdFrom,
@Param("createdTo") LocalDateTime createdTo,
@Param("unreplied") boolean unreplied,
Pageable pageable);
/** 管理端待审列表:PENDING,支持 jobId / riskLevel / 时间范围筛选 */ /** 管理端待审列表:PENDING,支持 jobId / riskLevel / 时间范围筛选 */
@Query("SELECT r FROM JobReview r WHERE r.status = 'PUBLISHED' AND r.moderationStatus = 'PENDING'" @Query("SELECT r FROM JobReview r WHERE r.status = 'PUBLISHED' AND r.moderationStatus = 'PENDING'"
+ " AND (:jobId IS NULL OR r.jobId = :jobId)" + " AND (:jobId IS NULL OR r.jobId = :jobId)"

26
mydreamjob-server/src/main/java/com/mydreamjob/review/JobReviewService.java

@ -43,6 +43,7 @@ public class JobReviewService {
private static final ObjectMapper mapper = new ObjectMapper(); private static final ObjectMapper mapper = new ObjectMapper();
private static final int MAX_PAGE_SIZE = 50; private static final int MAX_PAGE_SIZE = 50;
private static final Set<String> ALLOWED_STATUSES = Set.of("PUBLISHED", "HIDDEN"); private static final Set<String> ALLOWED_STATUSES = Set.of("PUBLISHED", "HIDDEN");
private static final Set<String> ALLOWED_RATING_TYPES = Set.of("GOOD", "MEDIUM", "BAD");
private static final int MIN_SCORE = 1; private static final int MIN_SCORE = 1;
private static final int MAX_SCORE = 5; private static final int MAX_SCORE = 5;
private static final int MAX_REQUEST_ID_LENGTH = 32; private static final int MAX_REQUEST_ID_LENGTH = 32;
@ -67,6 +68,16 @@ public class JobReviewService {
throw new BusinessException(ErrorCode.BAD_REQUEST); throw new BusinessException(ErrorCode.BAD_REQUEST);
} }
private void assertValidRatingType(String ratingType) {
if (ratingType != null && !ALLOWED_RATING_TYPES.contains(ratingType))
throw new BusinessException(ErrorCode.BAD_REQUEST);
}
private void assertValidTimeRange(LocalDateTime from, LocalDateTime to) {
if (from != null && to != null && from.isAfter(to))
throw new BusinessException(ErrorCode.BAD_REQUEST);
}
private void assertValidStatus(String status) { private void assertValidStatus(String status) {
if (status != null && !ALLOWED_STATUSES.contains(status)) if (status != null && !ALLOWED_STATUSES.contains(status))
throw new BusinessException(ErrorCode.BAD_REQUEST); throw new BusinessException(ErrorCode.BAD_REQUEST);
@ -141,11 +152,20 @@ public class JobReviewService {
// ── 查询 ────────────────────────────────────────────────── // ── 查询 ──────────────────────────────────────────────────
public Page<Map<String, Object>> listReviews(Long jobId, int page, int size) { public Page<Map<String, Object>> listReviews(Long jobId, int page, int size) {
return listReviews(jobId, null, null, null, false, page, size);
}
/** 公开评价列表(含筛选):评分档/创建时间区间/未回复;均可不传 */
public Page<Map<String, Object>> listReviews(Long jobId, String ratingType,
LocalDateTime createdFrom, LocalDateTime createdTo,
boolean unreplied, int page, int size) {
assertJobExists(jobId); assertJobExists(jobId);
assertValidPage(page, size); assertValidPage(page, size);
Page<JobReview> result = reviewRepository assertValidRatingType(ratingType);
.findByJobIdAndStatusAndModerationStatusInOrderByCreatedAtDesc( assertValidTimeRange(createdFrom, createdTo);
jobId, "PUBLISHED", PUBLIC_MODERATION_STATUSES, PageRequest.of(page, size)); Page<JobReview> result = reviewRepository.findPublicReviews(
jobId, "PUBLISHED", PUBLIC_MODERATION_STATUSES,
ratingType, createdFrom, createdTo, unreplied, PageRequest.of(page, size));
List<Long> ids = result.getContent().stream().map(JobReview::getId).toList(); List<Long> ids = result.getContent().stream().map(JobReview::getId).toList();
Map<Long, Map<String, Integer>> dimsByReview = batchLoadDimensionMap(ids); Map<Long, Map<String, Integer>> dimsByReview = batchLoadDimensionMap(ids);
return result.map(r -> toView(r, dimsByReview.getOrDefault(r.getId(), Map.of()))); return result.map(r -> toView(r, dimsByReview.getOrDefault(r.getId(), Map.of())));

2
mydreamjob-server/src/test/java/com/mydreamjob/review/JobReviewControllerTest.java

@ -66,7 +66,7 @@ class JobReviewControllerTest {
} }
@Test void listReviews_public_shouldReturn200() throws Exception { @Test void listReviews_public_shouldReturn200() throws Exception {
when(reviewService.listReviews(eq(JOB_ID), eq(0), eq(20))) when(reviewService.listReviews(eq(JOB_ID), any(), any(), any(), anyBoolean(), eq(0), eq(20)))
.thenReturn(new PageImpl<>(List.of(Map.of("rating", 4)))); .thenReturn(new PageImpl<>(List.of(Map.of("rating", 4))));
mockMvc.perform(get("/jobs/" + JOB_ID + "/reviews")) mockMvc.perform(get("/jobs/" + JOB_ID + "/reviews"))
.andExpect(status().isOk()).andExpect(jsonPath("$.code").value(200)); .andExpect(status().isOk()).andExpect(jsonPath("$.code").value(200));

57
mydreamjob-server/src/test/java/com/mydreamjob/review/JobReviewServiceTest.java

@ -18,11 +18,13 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageImpl;
import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@ -51,6 +53,7 @@ class JobReviewServiceTest {
.rating(4).content("不错的岗位").status("PUBLISHED").build(); .rating(4).content("不错的岗位").status("PUBLISHED").build();
// 维度相关默认空数据(旧用例不传 dimensionScores;lenient 避免部分用例未用到) // 维度相关默认空数据(旧用例不传 dimensionScores;lenient 避免部分用例未用到)
lenient().when(dimensionScoreRepository.findByReviewId(any())).thenReturn(List.of()); lenient().when(dimensionScoreRepository.findByReviewId(any())).thenReturn(List.of());
lenient().when(dimensionScoreRepository.findByReviewIdIn(any())).thenReturn(List.of());
lenient().when(dimensionScoreRepository.aggregateByDimension(any())).thenReturn(List.of()); lenient().when(dimensionScoreRepository.aggregateByDimension(any())).thenReturn(List.of());
// 企业回复默认无(toView 会查询) // 企业回复默认无(toView 会查询)
lenient().when(replyRepository.findByReviewIdAndModerationStatus(any(), any())) lenient().when(replyRepository.findByReviewIdAndModerationStatus(any(), any()))
@ -145,12 +148,64 @@ class JobReviewServiceTest {
@Test void listReviews_shouldReturnPublishedOnly() { @Test void listReviews_shouldReturnPublishedOnly() {
when(jobService.getJob(JOB_ID)).thenReturn(Job.builder().id(JOB_ID).build()); when(jobService.getJob(JOB_ID)).thenReturn(Job.builder().id(JOB_ID).build());
when(reviewRepository.findByJobIdAndStatusAndModerationStatusInOrderByCreatedAtDesc(eq(JOB_ID), eq("PUBLISHED"), any(), any())) when(reviewRepository.findPublicReviews(eq(JOB_ID), eq("PUBLISHED"), any(),
isNull(), isNull(), isNull(), eq(false), any()))
.thenReturn(new PageImpl<>(List.of(review))); .thenReturn(new PageImpl<>(List.of(review)));
var r = reviewService.listReviews(JOB_ID, 0, 20); var r = reviewService.listReviews(JOB_ID, 0, 20);
assertEquals(1, r.getTotalElements()); assertEquals(1, r.getTotalElements());
} }
// ── 列表筛选(好评/中评/差评/未回复/时间区间) ────────────
@Test void listReviews_withFilters_shouldPassToRepository() {
when(jobService.getJob(JOB_ID)).thenReturn(Job.builder().id(JOB_ID).build());
LocalDateTime from = LocalDateTime.of(2026, 1, 1, 0, 0);
LocalDateTime to = LocalDateTime.of(2026, 6, 1, 0, 0);
when(reviewRepository.findPublicReviews(eq(JOB_ID), eq("PUBLISHED"), any(),
eq("GOOD"), eq(from), eq(to), eq(true), any()))
.thenReturn(new PageImpl<>(List.of(review)));
var r = reviewService.listReviews(JOB_ID, "GOOD", from, to, true, 0, 20);
assertEquals(1, r.getTotalElements());
verify(reviewRepository).findPublicReviews(eq(JOB_ID), eq("PUBLISHED"), any(),
eq("GOOD"), eq(from), eq(to), eq(true), any());
}
@Test void listReviews_invalidRatingType_shouldReturn400() {
when(jobService.getJob(JOB_ID)).thenReturn(Job.builder().id(JOB_ID).build());
BusinessException ex = assertThrows(BusinessException.class,
() -> reviewService.listReviews(JOB_ID, "EXCELLENT",
LocalDateTime.now(), null, false, 0, 20));
assertEquals(ErrorCode.BAD_REQUEST.getCode(), ex.getErrorCodeInt());
}
@Test void listReviews_fromAfterTo_shouldReturn400() {
when(jobService.getJob(JOB_ID)).thenReturn(Job.builder().id(JOB_ID).build());
LocalDateTime from = LocalDateTime.of(2026, 6, 1, 0, 0);
LocalDateTime to = LocalDateTime.of(2026, 1, 1, 0, 0);
BusinessException ex = assertThrows(BusinessException.class,
() -> reviewService.listReviews(JOB_ID, null, from, to, false, 0, 20));
assertEquals(ErrorCode.BAD_REQUEST.getCode(), ex.getErrorCodeInt());
}
@Test void listReviews_onlyFromNoTo_shouldBeAllowed() {
when(jobService.getJob(JOB_ID)).thenReturn(Job.builder().id(JOB_ID).build());
LocalDateTime from = LocalDateTime.of(2026, 1, 1, 0, 0);
when(reviewRepository.findPublicReviews(eq(JOB_ID), eq("PUBLISHED"), any(),
isNull(), eq(from), isNull(), eq(false), any()))
.thenReturn(new PageImpl<>(List.of(review)));
var r = reviewService.listReviews(JOB_ID, null, from, null, false, 0, 20);
assertEquals(1, r.getTotalElements());
}
@Test void listReviews_nullFilters_shouldDefaultFalse() {
when(jobService.getJob(JOB_ID)).thenReturn(Job.builder().id(JOB_ID).build());
when(reviewRepository.findPublicReviews(eq(JOB_ID), eq("PUBLISHED"), any(),
isNull(), isNull(), isNull(), eq(false), any()))
.thenReturn(new PageImpl<>(List.of(review)));
var r = reviewService.listReviews(JOB_ID, null, null, null, false, 0, 20);
assertEquals(1, r.getTotalElements());
}
@Test void updateReview_shouldSucceed() { @Test void updateReview_shouldSucceed() {
mockStudentRole(STUDENT_ID); mockStudentRole(STUDENT_ID);
when(reviewRepository.findById(REVIEW_ID)).thenReturn(Optional.of(review)); when(reviewRepository.findById(REVIEW_ID)).thenReturn(Optional.of(review));

Loading…
Cancel
Save