forked from baskbuddy/zhangshun
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
230 lines
12 KiB
230 lines
12 KiB
package com.mydreamjob.careerprogress;
|
|
|
|
import com.mydreamjob.application.Application;
|
|
import com.mydreamjob.application.ApplicationRepository;
|
|
import com.mydreamjob.auth.User;
|
|
import com.mydreamjob.auth.UserRepository;
|
|
import com.mydreamjob.candidatescore.CandidateDimensionScore;
|
|
import com.mydreamjob.candidatescore.CandidateDimensionScoreRepository;
|
|
import com.mydreamjob.careerprogress.dto.CareerProgressResponse;
|
|
import com.mydreamjob.company.Company;
|
|
import com.mydreamjob.company.CompanyRepository;
|
|
import com.mydreamjob.interview.Interview;
|
|
import com.mydreamjob.interview.InterviewRepository;
|
|
import com.mydreamjob.interviewscore.InterviewScore;
|
|
import com.mydreamjob.interviewscore.InterviewScoreRepository;
|
|
import com.mydreamjob.job.Job;
|
|
import com.mydreamjob.job.JobRepository;
|
|
import com.mydreamjob.notification.Notification;
|
|
import com.mydreamjob.notification.NotificationRepository;
|
|
import com.mydreamjob.offer.Offer;
|
|
import com.mydreamjob.offer.OfferRepository;
|
|
import org.junit.jupiter.api.AfterEach;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.boot.test.context.SpringBootTest;
|
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
|
import org.springframework.data.redis.core.RedisTemplate;
|
|
import org.springframework.data.redis.core.ValueOperations;
|
|
import org.springframework.test.context.ActiveProfiles;
|
|
import org.springframework.test.context.TestPropertySource;
|
|
|
|
import java.math.BigDecimal;
|
|
import java.time.LocalDate;
|
|
import java.time.LocalDateTime;
|
|
import java.util.Set;
|
|
import java.util.concurrent.atomic.AtomicLong;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
import static org.mockito.ArgumentMatchers.anyString;
|
|
import static org.mockito.Mockito.mock;
|
|
import static org.mockito.Mockito.when;
|
|
|
|
/**
|
|
* 统一求职进度聚合持久化/只读集成测试(V34,独立 H2 + Flyway 真实表结构)。
|
|
*
|
|
* <p>证明:真实批量查询在真实 Repository 上正确返回聚合结果;CareerProgressService
|
|
* 标注 @Transactional(readOnly=true) 且不写业务表 —— 调用前后关键业务表计数不变;
|
|
* 不新增 Flyway、不删除业务数据。</p>
|
|
*/
|
|
@SpringBootTest
|
|
@TestPropertySource(properties =
|
|
"spring.datasource.url=jdbc:h2:mem:integrationdb_careerprogress_persist;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE")
|
|
@ActiveProfiles("integration-test")
|
|
class CareerProgressPersistenceIntegrationTest {
|
|
|
|
@Autowired private CareerProgressService service;
|
|
@Autowired private UserRepository userRepository;
|
|
@Autowired private CompanyRepository companyRepository;
|
|
@Autowired private JobRepository jobRepository;
|
|
@Autowired private ApplicationRepository applicationRepository;
|
|
@Autowired private InterviewRepository interviewRepository;
|
|
@Autowired private InterviewScoreRepository interviewScoreRepository;
|
|
@Autowired private OfferRepository offerRepository;
|
|
@Autowired private NotificationRepository notificationRepository;
|
|
@Autowired private CandidateDimensionScoreRepository dimensionScoreRepository;
|
|
|
|
@MockBean private RedisTemplate<String, Object> redisTemplate;
|
|
|
|
private static final AtomicLong PHONE_SEQ = new AtomicLong(600);
|
|
private String period;
|
|
|
|
private Long studentUserId;
|
|
private Long companyUserId;
|
|
private Long companyId;
|
|
private Long jobId;
|
|
private Long appId;
|
|
|
|
@BeforeEach
|
|
@SuppressWarnings("unchecked")
|
|
void setUp() {
|
|
ValueOperations<String, Object> ops = mock(ValueOperations.class);
|
|
when(redisTemplate.opsForValue()).thenReturn(ops);
|
|
when(redisTemplate.hasKey(anyString())).thenReturn(false);
|
|
|
|
period = String.format("%04d-%02d", LocalDate.now().getYear(), LocalDate.now().getMonthValue());
|
|
|
|
studentUserId = createUser(User.Role.STUDENT);
|
|
companyUserId = createUser(User.Role.COMPANY);
|
|
Long rivalId = createUser(User.Role.STUDENT);
|
|
|
|
companyId = companyRepository.save(
|
|
Company.builder().ownerUserId(companyUserId).name("示例科技").build()).getId();
|
|
jobId = jobRepository.save(Job.builder().companyId(companyId).title("Java 后端工程师")
|
|
.description("d").city("杭州").employmentType("FULL_TIME").status("PUBLISHED").build()).getId();
|
|
|
|
Application app = applicationRepository.save(Application.builder()
|
|
.jobId(jobId).studentUserId(studentUserId).status("INTERVIEW")
|
|
.createdAt(LocalDateTime.now().minusDays(1)).updatedAt(LocalDateTime.now()).build());
|
|
appId = app.getId();
|
|
applicationRepository.save(Application.builder()
|
|
.jobId(jobId).studentUserId(rivalId).status("SUBMITTED")
|
|
.createdAt(LocalDateTime.now().minusDays(2)).updatedAt(LocalDateTime.now()).build());
|
|
|
|
// 竞争力快照
|
|
dimensionScoreRepository.save(dim(studentUserId, "77.50"));
|
|
dimensionScoreRepository.save(dim(rivalId, "90.00"));
|
|
|
|
// 面试:SCHEDULED(待响应)+ CONFIRMED(未来)
|
|
Interview sched = interviewRepository.save(interview("SCHEDULED", LocalDateTime.now().plusDays(1), 2));
|
|
interviewRepository.save(interview("CONFIRMED", LocalDateTime.now().plusDays(2), 3));
|
|
|
|
// 已提交评分(摘要口径,不返回 comment)
|
|
interviewScoreRepository.save(InterviewScore.builder()
|
|
.interviewId(sched.getId()).applicationId(appId).companyUserId(companyUserId)
|
|
.studentUserId(studentUserId).status(InterviewScore.STATUS_SUBMITTED)
|
|
.technicalScore(4).communicationScore(3).problemSolvingScore(4)
|
|
.roleFitScore(4).professionalismScore(3)
|
|
.overallScore(new BigDecimal("3.60")).recommendation("HIRE")
|
|
.comment("不应返回给学生").submittedAt(LocalDateTime.now()).version(0L).build());
|
|
|
|
// 有效 Offer
|
|
offerRepository.save(Offer.builder()
|
|
.applicationId(appId).jobId(jobId).companyId(companyId).candidateId(studentUserId)
|
|
.title("Java 后端工程师").description("d").expireAt(LocalDateTime.now().plusDays(9))
|
|
.salaryMin(20000).salaryMax(30000).baseSalary(25000)
|
|
.status("SENT").version(0L).createdBy(companyUserId).build());
|
|
|
|
// 通知:未读 2 条(其中 1 条 REPORT_DECISION)
|
|
notificationRepository.save(Notification.builder().userId(studentUserId).type("INTERVIEW_SCHEDULED")
|
|
.title("面试通知").content("你有一场面试").build());
|
|
notificationRepository.save(Notification.builder().userId(studentUserId).type("REPORT_DECISION")
|
|
.title("举报处理结果").content("举报已处理").build());
|
|
}
|
|
|
|
@AfterEach
|
|
void tearDown() {
|
|
dimensionScoreRepository.deleteAll();
|
|
interviewScoreRepository.deleteAll();
|
|
offerRepository.deleteAll();
|
|
interviewRepository.deleteAll();
|
|
notificationRepository.deleteAll();
|
|
applicationRepository.deleteAll();
|
|
jobRepository.deleteAll();
|
|
companyRepository.deleteAll();
|
|
userRepository.deleteAll();
|
|
}
|
|
|
|
@Test
|
|
void aggregation_returnsCorrectSummaries_andIsReadOnly() {
|
|
long appsBefore = applicationRepository.count();
|
|
long interviewsBefore = interviewRepository.count();
|
|
long scoresBefore = interviewScoreRepository.count();
|
|
long offersBefore = offerRepository.count();
|
|
long notificationsBefore = notificationRepository.count();
|
|
long dimsBefore = dimensionScoreRepository.count();
|
|
|
|
CareerProgressResponse resp = service.getCareerProgress(studentUserId, 0, 20, null);
|
|
|
|
// todo 计数(真实批量查询)
|
|
assertEquals(1L, resp.getTodo().getPendingInterviewResponseCount());
|
|
assertEquals(1L, resp.getTodo().getUpcomingConfirmedInterviewCount());
|
|
assertEquals(1L, resp.getTodo().getPendingOfferResponseCount());
|
|
assertEquals(2L, resp.getTodo().getUnreadNotificationCount());
|
|
assertEquals(1L, resp.getTodo().getUnreadReportDecisionCount());
|
|
|
|
// 投递分页(本人仅 1 条,不包含 rival 投递)
|
|
assertEquals(1L, resp.getApplications().getTotal());
|
|
assertEquals(1, resp.getApplications().getItems().size());
|
|
CareerProgressResponse.CompetitionSummary comp =
|
|
resp.getApplications().getItems().get(0).getCompetition();
|
|
assertTrue(comp.isEligible());
|
|
assertEquals(2, comp.getCohortSize());
|
|
assertEquals(2, comp.getRank().intValue());
|
|
assertEquals(new BigDecimal("77.50"), comp.getOverallScore());
|
|
assertEquals(new BigDecimal("0.00"), comp.getPercentile());
|
|
assertEquals(period, comp.getMySnapshotPeriod());
|
|
assertNotNull(resp.getGeneratedAt());
|
|
|
|
// 只读:调用前后关键业务表计数完全不变(不写、不删、不新增)
|
|
assertEquals(appsBefore, applicationRepository.count());
|
|
assertEquals(interviewsBefore, interviewRepository.count());
|
|
assertEquals(scoresBefore, interviewScoreRepository.count());
|
|
assertEquals(offersBefore, offerRepository.count());
|
|
assertEquals(notificationsBefore, notificationRepository.count());
|
|
assertEquals(dimsBefore, dimensionScoreRepository.count());
|
|
}
|
|
|
|
@Test
|
|
void withdrawnApplication_keptInHistory_competitionNotEligible() {
|
|
// 独立岗位避免 (job_id, student_user_id) 唯一约束冲突
|
|
Long withdrawnJobId = jobRepository.save(Job.builder().companyId(companyId).title("已撤回岗位")
|
|
.description("d").city("上海").employmentType("FULL_TIME").status("PUBLISHED").build()).getId();
|
|
applicationRepository.save(Application.builder()
|
|
.jobId(withdrawnJobId).studentUserId(studentUserId).status("WITHDRAWN")
|
|
.createdAt(LocalDateTime.now().minusDays(3)).updatedAt(LocalDateTime.now()).build());
|
|
|
|
CareerProgressResponse resp = service.getCareerProgress(studentUserId, 0, 20, null);
|
|
// 现在本人有两条投递(INTERVIEW + WITHDRAWN)
|
|
assertEquals(2L, resp.getApplications().getTotal());
|
|
CareerProgressResponse.CareerProgressItem withdrawn = resp.getApplications().getItems().stream()
|
|
.filter(i -> "WITHDRAWN".equals(i.getApplicationStatus())).findFirst().orElseThrow();
|
|
assertEquals("WITHDRAWN", withdrawn.getApplicationStatus());
|
|
assertEquals(false, withdrawn.getCompetition().isEligible());
|
|
assertEquals(null, withdrawn.getCompetition().getRank());
|
|
assertEquals(null, withdrawn.getCompetition().getCohortSize());
|
|
assertEquals(null, withdrawn.getCompetition().getOverallScore());
|
|
assertEquals(null, withdrawn.getCompetition().getPercentile());
|
|
}
|
|
|
|
private Long createUser(User.Role role) {
|
|
return userRepository.save(User.builder()
|
|
.phone("168" + String.format("%08d", PHONE_SEQ.incrementAndGet()))
|
|
.nickname("u").role(role).roles(Set.of(role)).active(true).verified(true).build()).getId();
|
|
}
|
|
|
|
private Interview interview(String status, LocalDateTime startAt, int roundNo) {
|
|
return Interview.builder().applicationId(appId).companyUserId(companyUserId).studentUserId(studentUserId)
|
|
.roundNo(roundNo).title("技术面试").interviewType("ONLINE").startAt(startAt)
|
|
.endAt(startAt.plusHours(1)).status(status).build();
|
|
}
|
|
|
|
private CandidateDimensionScore dim(Long sid, String score) {
|
|
return CandidateDimensionScore.builder().studentUserId(sid).dimension("EDUCATION").period(period)
|
|
.score(new BigDecimal(score)).status("OK").weight(new BigDecimal("12.50"))
|
|
.rulesVersion("candidate-scoring-v0.1").calculatedAt(LocalDateTime.now()).build();
|
|
}
|
|
}
|