Browse Source

岗位评价管理 评价回复率模块功能

backup/master-local-20261012
pwklppg 2 months ago
parent
commit
d00a63069d
  1. 9
      mydreamjob-server/src/main/java/com/mydreamjob/review/CompanyReviewReplyController.java
  2. 18
      mydreamjob-server/src/main/java/com/mydreamjob/review/CompanyReviewReplyRepository.java
  3. 48
      mydreamjob-server/src/main/java/com/mydreamjob/review/CompanyReviewReplyService.java
  4. 22
      mydreamjob-server/src/main/java/com/mydreamjob/review/dto/ReviewReplyStatsResponse.java
  5. 66
      mydreamjob-server/src/test/java/com/mydreamjob/review/CompanyReviewReplyServiceTest.java

9
mydreamjob-server/src/main/java/com/mydreamjob/review/CompanyReviewReplyController.java

@ -4,6 +4,7 @@ import com.mydreamjob.common.api.ApiResponse;
import com.mydreamjob.review.dto.CompanyReviewReplyCreateRequest;
import com.mydreamjob.review.dto.CompanyReviewReplyResponse;
import com.mydreamjob.review.dto.CompanyReviewReplyUpdateRequest;
import com.mydreamjob.review.dto.ReviewReplyStatsResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
@ -66,6 +67,14 @@ public class CompanyReviewReplyController {
return ResponseEntity.ok(ApiResponse.ok(Map.of("message", "reply deleted"), getRequestId(request)));
}
@Operation(summary = "企业回复率统计", description = "仅企业;返回本公司本月回复率、行业平均回复率、本月待回复条数")
@GetMapping("/company/review-reply/stats")
public ResponseEntity<ApiResponse<ReviewReplyStatsResponse>> stats(HttpServletRequest request) {
Long userId = resolveUserId(request);
ReviewReplyStatsResponse response = replyService.getReviewReplyStats(userId);
return ResponseEntity.ok(ApiResponse.ok(response, getRequestId(request)));
}
private Long resolveUserId(HttpServletRequest request) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !auth.isAuthenticated() || auth instanceof AnonymousAuthenticationToken) {

18
mydreamjob-server/src/main/java/com/mydreamjob/review/CompanyReviewReplyRepository.java

@ -8,6 +8,7 @@ import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
@ -39,4 +40,21 @@ public interface CompanyReviewReplyRepository extends JpaRepository<CompanyRevie
@Param("createdFrom") LocalDateTime createdFrom,
@Param("createdTo") LocalDateTime createdTo,
Pageable pageable);
/** 按月统计同一行业下各企业评价总数与已回复数;industry 为 null 时不限行业 */
@Query("SELECT j.companyId AS companyId, COUNT(r) AS totalCnt, "
+ " COUNT(CASE WHEN rr.id IS NOT NULL THEN 1 END) AS repliedCnt "
+ " FROM JobReview r "
+ " JOIN Job j ON j.id = r.jobId "
+ " LEFT JOIN CompanyReviewReply rr ON rr.reviewId = r.id "
+ " WHERE (:industry IS NULL OR"
+ " j.companyId IN (SELECT cc.id FROM Company cc WHERE cc.industry = :industry))"
+ " AND r.status = 'PUBLISHED'"
+ " AND r.moderationStatus IN :moderationStatuses"
+ " AND r.createdAt >= :start AND r.createdAt < :end"
+ " GROUP BY j.companyId")
List<Object[]> findMonthlyReplyStats(@Param("industry") String industry,
@Param("moderationStatuses") Collection<String> moderationStatuses,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
}

48
mydreamjob-server/src/main/java/com/mydreamjob/review/CompanyReviewReplyService.java

@ -13,6 +13,7 @@ import com.mydreamjob.review.dto.AdminCompanyReviewReplyResponse;
import com.mydreamjob.review.dto.CompanyReviewReplyCreateRequest;
import com.mydreamjob.review.dto.CompanyReviewReplyResponse;
import com.mydreamjob.review.dto.CompanyReviewReplyUpdateRequest;
import com.mydreamjob.review.dto.ReviewReplyStatsResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
@ -21,6 +22,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -137,6 +139,52 @@ public class CompanyReviewReplyService {
return result;
}
// ── 回复率统计(企业侧) ──────────────────────────────────
/**
* 企业回复率统计:本月回复率 / 行业平均回复率(逐企业平均)/ 本月待回复条数。
* 仅统计本月、属于本公司岗位、可回复的公开评价(AUTO_APPROVED/APPROVED);
* 评价存在任意回复记录即视为已回复。
*/
@Transactional(readOnly = true)
public ReviewReplyStatsResponse getReviewReplyStats(Long userId) {
Company company = companyRepository.findByOwnerUserId(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.FORBIDDEN));
LocalDateTime start = LocalDate.now().withDayOfMonth(1).atStartOfDay();
LocalDateTime end = start.plusMonths(1);
String industry = company.getIndustry();
boolean hasIndustry = industry != null && !industry.isBlank();
List<Object[]> rows = replyRepository.findMonthlyReplyStats(
hasIndustry ? industry : null,
List.of(JobReview.MODERATION_AUTO_APPROVED, JobReview.MODERATION_APPROVED),
start, end);
long myTotal = 0, myReplied = 0;
double rateSum = 0;
long rateCount = 0;
for (Object[] row : rows) {
long cid = ((Number) row[0]).longValue();
long total = ((Number) row[1]).longValue();
long replied = ((Number) row[2]).longValue();
if (cid == company.getId()) {
myTotal = total;
myReplied = replied;
}
if (total > 0) {
rateSum += (double) replied / total;
rateCount++;
}
}
int monthRate = myTotal == 0 ? 0 : (int) Math.round(myReplied * 100.0 / myTotal);
int unreplied = (int) (myTotal - myReplied);
int industryAvg = (!hasIndustry || rateCount == 0)
? 0 : (int) Math.round(rateSum / rateCount * 100.0);
return ReviewReplyStatsResponse.builder()
.monthReplyRate(monthRate)
.industryAvgReplyRate(industryAvg)
.unrepliedCount(unreplied)
.build();
}
// ── 更新回复(企业侧) ──────────────────────────────────────
@Transactional

22
mydreamjob-server/src/main/java/com/mydreamjob/review/dto/ReviewReplyStatsResponse.java

@ -0,0 +1,22 @@
package com.mydreamjob.review.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 企业回复率统计响应:本月回复率 / 行业平均回复率 / 本月待回复条数
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ReviewReplyStatsResponse {
/** 本月回复率(百分比整数,如 80 表示 80%) */
private Integer monthReplyRate;
/** 行业平均回复率(百分比整数) */
private Integer industryAvgReplyRate;
/** 本月待回复评价条数 */
private Integer unrepliedCount;
}

66
mydreamjob-server/src/test/java/com/mydreamjob/review/CompanyReviewReplyServiceTest.java

@ -12,6 +12,7 @@ import com.mydreamjob.job.JobService;
import com.mydreamjob.review.dto.AdminCompanyReviewReplyResponse;
import com.mydreamjob.review.dto.CompanyReviewReplyCreateRequest;
import com.mydreamjob.review.dto.CompanyReviewReplyUpdateRequest;
import com.mydreamjob.review.dto.ReviewReplyStatsResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -395,6 +396,71 @@ class CompanyReviewReplyServiceTest {
assertEquals(COMPANY_USER_ID, v.getCompanyUserId());
}
// ── 回复率统计(企业侧) ──────────────────────────────────
@Test void getReviewReplyStats_shouldComputeMonthRateAndIndustryAvg() {
when(companyRepository.findByOwnerUserId(COMPANY_USER_ID))
.thenReturn(Optional.of(Company.builder().id(COMPANY_ID)
.ownerUserId(COMPANY_USER_ID).industry("IT").build()));
when(replyRepository.findMonthlyReplyStats(any(), any(), any(), any())).thenReturn(List.of(
new Object[]{COMPANY_ID, 5L, 4L}, // 当前企业:80%
new Object[]{OTHER_COMPANY_ID, 4L, 2L})); // 同行企业:50%
ReviewReplyStatsResponse r = replyService.getReviewReplyStats(COMPANY_USER_ID);
assertEquals(80, r.getMonthReplyRate());
assertEquals(65, r.getIndustryAvgReplyRate()); // (0.8+0.5)/2 = 0.65
assertEquals(1, r.getUnrepliedCount());
}
@Test void getReviewReplyStats_noReplies_shouldReturnZeroRate() {
when(companyRepository.findByOwnerUserId(COMPANY_USER_ID))
.thenReturn(Optional.of(Company.builder().id(COMPANY_ID)
.ownerUserId(COMPANY_USER_ID).industry("IT").build()));
when(replyRepository.findMonthlyReplyStats(any(), any(), any(), any()))
.thenReturn(List.<Object[]>of(new Object[]{COMPANY_ID, 2L, 0L}));
ReviewReplyStatsResponse r = replyService.getReviewReplyStats(COMPANY_USER_ID);
assertEquals(0, r.getMonthReplyRate());
assertEquals(2, r.getUnrepliedCount());
}
@Test void getReviewReplyStats_noReviewsThisMonth_shouldReturnZeros() {
when(companyRepository.findByOwnerUserId(COMPANY_USER_ID))
.thenReturn(Optional.of(Company.builder().id(COMPANY_ID)
.ownerUserId(COMPANY_USER_ID).industry("IT").build()));
when(replyRepository.findMonthlyReplyStats(any(), any(), any(), any())).thenReturn(List.of());
ReviewReplyStatsResponse r = replyService.getReviewReplyStats(COMPANY_USER_ID);
assertEquals(0, r.getMonthReplyRate());
assertEquals(0, r.getIndustryAvgReplyRate());
assertEquals(0, r.getUnrepliedCount());
}
@Test void getReviewReplyStats_companyWithoutIndustry_shouldSetIndustryAvgZero() {
when(companyRepository.findByOwnerUserId(COMPANY_USER_ID))
.thenReturn(Optional.of(Company.builder().id(COMPANY_ID)
.ownerUserId(COMPANY_USER_ID).industry(null).build()));
when(replyRepository.findMonthlyReplyStats(any(), any(), any(), any()))
.thenReturn(List.<Object[]>of(new Object[]{COMPANY_ID, 5L, 4L}));
ReviewReplyStatsResponse r = replyService.getReviewReplyStats(COMPANY_USER_ID);
assertEquals(80, r.getMonthReplyRate());
assertEquals(0, r.getIndustryAvgReplyRate());
assertEquals(1, r.getUnrepliedCount());
}
@Test void getReviewReplyStats_companyUserWithoutCompany_shouldReturn403() {
when(companyRepository.findByOwnerUserId(COMPANY_USER_ID)).thenReturn(Optional.empty());
BusinessException ex = assertThrows(BusinessException.class,
() -> replyService.getReviewReplyStats(COMPANY_USER_ID));
assertEquals(ErrorCode.FORBIDDEN.getCode(), ex.getErrorCodeInt());
}
private Long ADMIN_ROLE_ID() {
return 1L;
}

Loading…
Cancel
Save