| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472 |
- package com.emoon.okr.service;
- import com.emoon.okr.entity.KpiConfig;
- import com.emoon.okr.entity.KpiTemplate;
- import com.emoon.okr.entity.KpiTemplateAssignment;
- import com.emoon.okr.entity.OkrObjective;
- import com.emoon.okr.entity.AssessmentPeriod;
- import com.emoon.okr.entity.PeriodParticipant;
- import com.emoon.okr.entity.SysUser;
- import com.emoon.okr.enums.PeriodStatus;
- import com.emoon.okr.enums.ScoreType;
- import com.emoon.okr.enums.UserRole;
- import com.emoon.okr.exception.BusinessException;
- import com.emoon.okr.mapper.*;
- import org.junit.jupiter.api.Test;
- import org.junit.jupiter.api.BeforeEach;
- import org.junit.jupiter.api.extension.ExtendWith;
- import org.mockito.InjectMocks;
- import org.mockito.Mock;
- import org.mockito.junit.jupiter.MockitoExtension;
- import org.springframework.context.ApplicationEventPublisher;
- import java.util.List;
- import java.util.ArrayList;
- import java.util.Map;
- import java.time.LocalDateTime;
- import static org.junit.jupiter.api.Assertions.*;
- import static org.mockito.ArgumentMatchers.any;
- import static org.mockito.Mockito.*;
- @ExtendWith(MockitoExtension.class)
- class AssessmentPeriodServiceTest {
- @Mock AssessmentPeriodMapper periodMapper;
- @Mock SysUserMapper userMapper;
- @Mock OkrObjectiveMapper objectiveMapper;
- @Mock KrKeyResultMapper krMapper;
- @Mock KpiConfigMapper kpiConfigMapper;
- @Mock KpiTemplateMapper kpiTemplateMapper;
- @Mock KpiTemplateAssignmentMapper kpiTemplateAssignmentMapper;
- @Mock PerformanceScoreMapper scoreMapper;
- @Mock PerformanceFeedbackMapper feedbackMapper;
- @Mock PeriodParticipantMapper participantMapper;
- @Mock AssessmentDimensionMapper dimensionMapper;
- @Mock PeriodDimensionMapper periodDimensionMapper;
- @Mock ScoreAppealMapper appealMapper;
- @Mock ApplicationEventPublisher publisher;
- @InjectMocks AssessmentPeriodService periodService;
- @BeforeEach
- void defaultFortyPointDimensions() {
- var dimensions = new ArrayList<com.emoon.okr.entity.AssessmentDimension>();
- for (int i = 0; i < 4; i++) {
- var dimension = new com.emoon.okr.entity.AssessmentDimension();
- dimension.setId((long) i + 1);
- dimension.setName("维度" + i);
- dimension.setMaxScore(10);
- dimension.setSortOrder(i);
- dimensions.add(dimension);
- }
- lenient().when(dimensionMapper.selectList(any())).thenReturn(dimensions);
- }
- @Test
- void transitionToExecutingRejectsPendingOkrReview() {
- AssessmentPeriod period = period(1L, PeriodStatus.OKR_ALIGN);
- when(periodMapper.selectById(1L)).thenReturn(period);
- when(objectiveMapper.selectCount(any())).thenReturn(1L);
- BusinessException ex = assertThrows(BusinessException.class,
- () -> periodService.transitionStatus(1L, PeriodStatus.EXECUTING));
- assertTrue(ex.getMessage().contains("待审核OKR"));
- verify(periodMapper, never()).updateById(any());
- }
- @Test
- void transitionToArchivedRejectsUnconfirmedFinalScores() {
- AssessmentPeriod period = period(2L, PeriodStatus.ASSESSING);
- when(periodMapper.selectById(2L)).thenReturn(period);
- when(scoreMapper.selectCount(any())).thenReturn(1L, 1L);
- BusinessException ex = assertThrows(BusinessException.class,
- () -> periodService.transitionStatus(2L, PeriodStatus.ARCHIVED));
- assertTrue(ex.getMessage().contains("未确认最终评分"));
- verify(periodMapper, never()).updateById(any());
- }
- @Test
- void transitionFromExecutingCannotSkipAssessmentAndArchiveDirectly() {
- AssessmentPeriod period = period(9L, PeriodStatus.EXECUTING);
- when(periodMapper.selectById(9L)).thenReturn(period);
- BusinessException ex = assertThrows(BusinessException.class,
- () -> periodService.transitionStatus(9L, PeriodStatus.ARCHIVED));
- assertTrue(ex.getMessage().contains("进入考评"));
- verify(periodMapper, never()).updateById(any());
- }
- @Test
- void transitionToExecutingRejectsParticipantsWithoutApprovedOkr() {
- AssessmentPeriod period = period(3L, PeriodStatus.OKR_ALIGN);
- when(periodMapper.selectById(3L)).thenReturn(period);
- when(objectiveMapper.selectCount(any())).thenReturn(0L);
- when(kpiConfigMapper.selectCount(any())).thenReturn(0L);
- OkrObjective approved = new OkrObjective();
- approved.setUserId(10L);
- approved.setStatus("APPROVED");
- when(objectiveMapper.selectList(any())).thenReturn(List.of(approved));
- KpiConfig locked10 = lockedKpi(10L);
- KpiConfig locked11 = lockedKpi(11L);
- when(kpiConfigMapper.selectList(any())).thenReturn(List.of(locked10, locked11));
- BusinessException ex = assertThrows(BusinessException.class,
- () -> periodService.transitionStatus(3L, PeriodStatus.EXECUTING));
- assertTrue(ex.getMessage().contains("未提交或未通过OKR"));
- verify(periodMapper, never()).updateById(any());
- }
- @Test
- void transitionToArchivedRejectsMissingSelfScoreAndFeedbackReply() {
- AssessmentPeriod period = period(4L, PeriodStatus.ASSESSING);
- when(periodMapper.selectById(4L)).thenReturn(period);
- when(scoreMapper.selectCount(any())).thenReturn(1L, 0L);
- when(kpiConfigMapper.selectList(any())).thenReturn(List.of(lockedKpi(10L)));
- BusinessException ex = assertThrows(BusinessException.class,
- () -> periodService.transitionStatus(4L, PeriodStatus.ARCHIVED));
- assertTrue(ex.getMessage().contains("未完成自评"));
- verify(periodMapper, never()).updateById(any());
- }
- @Test
- void readinessUsesOnlyUsersWithPeriodDataAsParticipants() {
- AssessmentPeriod period = period(5L, PeriodStatus.OKR_ALIGN);
- when(periodMapper.selectById(5L)).thenReturn(period);
- when(objectiveMapper.selectCount(any())).thenReturn(0L);
- when(kpiConfigMapper.selectCount(any())).thenReturn(0L);
- OkrObjective approved = new OkrObjective();
- approved.setUserId(10L);
- approved.setStatus("APPROVED");
- when(objectiveMapper.selectList(any())).thenReturn(List.of(approved));
- when(kpiConfigMapper.selectList(any())).thenReturn(List.of(lockedKpi(10L)));
- var readiness = periodService.checkReadiness(5L, PeriodStatus.EXECUTING);
- assertTrue(readiness.isReady());
- assertTrue(readiness.getBlockers().isEmpty());
- verify(userMapper, never()).selectList(any());
- }
- @Test
- void transitionToOkrAlignCreatesActiveParticipantSnapshotForActiveDepartmentUsers() {
- AssessmentPeriod period = period(6L, PeriodStatus.DRAFT);
- when(periodMapper.selectById(6L)).thenReturn(period);
- SysUser active = participant(10L);
- active.setStatus("ACTIVE");
- SysUser disabled = participant(11L);
- disabled.setStatus("DISABLED");
- SysUser ceo = participant(99L);
- ceo.setSuperiorId(null);
- ceo.setStatus("ACTIVE");
- when(userMapper.selectList(any())).thenReturn(List.of(ceo, active, disabled));
- periodService.transitionStatus(6L, PeriodStatus.OKR_ALIGN);
- verify(participantMapper).insert(argThat(p ->
- p instanceof PeriodParticipant
- && ((PeriodParticipant) p).getPeriodId().equals(6L)
- && ((PeriodParticipant) p).getUserId().equals(10L)
- && "ACTIVE".equals(((PeriodParticipant) p).getStatus())));
- verify(participantMapper, never()).insert(argThat(p ->
- p instanceof PeriodParticipant && ((PeriodParticipant) p).getUserId().equals(11L)));
- }
- @Test
- void transitionToOkrAlignFreezesReportingLineAndExemptsUserWithoutEvaluator() {
- AssessmentPeriod period = period(8L, PeriodStatus.DRAFT);
- when(periodMapper.selectById(8L)).thenReturn(period);
- SysUser ceo = participant(20L);
- ceo.setPosition("CEO");
- ceo.setStatus("ACTIVE");
- ceo.setSuperiorId(null);
- SysUser employee = participant(21L);
- employee.setPosition("研发工程师");
- employee.setStatus("ACTIVE");
- employee.setSuperiorId(20L);
- when(userMapper.selectList(any())).thenReturn(List.of(ceo, employee));
- List<PeriodParticipant> inserted = new ArrayList<>();
- doAnswer(invocation -> {
- inserted.add(invocation.getArgument(0));
- return 1;
- }).when(participantMapper).insert(any());
- periodService.transitionStatus(8L, PeriodStatus.OKR_ALIGN);
- PeriodParticipant ceoSnapshot = inserted.stream()
- .filter(p -> p.getUserId().equals(20L)).findFirst().orElseThrow();
- PeriodParticipant employeeSnapshot = inserted.stream()
- .filter(p -> p.getUserId().equals(21L)).findFirst().orElseThrow();
- assertEquals("EXEMPT_NO_EVALUATOR", ceoSnapshot.getStatus());
- assertEquals("ACTIVE", employeeSnapshot.getStatus());
- assertEquals(20L, readField(employeeSnapshot, "superiorIdSnapshot"));
- assertEquals(20L, readField(employeeSnapshot, "evaluatorId"));
- assertEquals(1L, readField(employeeSnapshot, "departmentIdSnapshot"));
- assertEquals("研发工程师", readField(employeeSnapshot, "positionSnapshot"));
- assertEquals("EMPLOYEE", readField(employeeSnapshot, "roleSnapshot"));
- }
- @Test
- void transitionToOkrAlignInstantiatesAndLocksExplicitlyAssignedPerformanceTemplate() {
- AssessmentPeriod period = period(10L, PeriodStatus.DRAFT);
- period.setCompanyOwnerUserId(20L);
- when(periodMapper.selectById(10L)).thenReturn(period);
- SysUser ceo = participant(20L);
- ceo.setSuperiorId(null);
- ceo.setStatus("ACTIVE");
- ceo.setPosition("CEO");
- SysUser employee = participant(21L);
- employee.setSuperiorId(20L);
- employee.setStatus("ACTIVE");
- employee.setPosition("研发工程师");
- when(userMapper.selectList(any())).thenReturn(List.of(ceo, employee));
- PeriodParticipant assignedEmployee = participantRecord(10L, 21L, "ACTIVE");
- assignedEmployee.setDepartmentIdSnapshot(employee.getDepartmentId());
- assignedEmployee.setPositionSnapshot(employee.getPosition());
- when(participantMapper.selectList(any())).thenReturn(List.of(assignedEmployee));
- KpiTemplateAssignment assignment = new KpiTemplateAssignment();
- assignment.setTemplateId(5L);
- assignment.setTargetType("POSITION");
- assignment.setTargetValue("研发工程师");
- assignment.setPriority(10);
- when(kpiTemplateAssignmentMapper.selectList(any())).thenReturn(List.of(assignment));
- KpiTemplate template = new KpiTemplate();
- template.setId(5L);
- template.setItemsJson("[{\"name\":\"交付质量\",\"maxScore\":40}]");
- when(kpiTemplateMapper.selectById(5L)).thenReturn(template);
- periodService.transitionStatus(10L, PeriodStatus.OKR_ALIGN);
- verify(kpiConfigMapper).insert(argThat(config -> {
- KpiConfig value = (KpiConfig) config;
- return Long.valueOf(10L).equals(value.getPeriodId())
- && Long.valueOf(21L).equals(value.getUserId())
- && value.getLockedAt() != null
- && template.getItemsJson().equals(value.getItemsJson());
- }));
- }
- @Test
- void readinessIgnoresExemptParticipant() {
- AssessmentPeriod period = period(7L, PeriodStatus.OKR_ALIGN);
- when(periodMapper.selectById(7L)).thenReturn(period);
- when(objectiveMapper.selectCount(any())).thenReturn(0L);
- when(kpiConfigMapper.selectCount(any())).thenReturn(0L);
- PeriodParticipant active = participantRecord(7L, 10L, "ACTIVE");
- PeriodParticipant exempt = participantRecord(7L, 11L, "EXEMPT");
- when(participantMapper.selectList(any())).thenReturn(List.of(active, exempt));
- OkrObjective approved = new OkrObjective();
- approved.setUserId(10L);
- approved.setStatus("APPROVED");
- when(objectiveMapper.selectList(any())).thenReturn(List.of(approved));
- when(kpiConfigMapper.selectList(any())).thenReturn(List.of(lockedKpi(10L)));
- var readiness = periodService.checkReadiness(7L, PeriodStatus.EXECUTING);
- assertTrue(readiness.isReady());
- }
- @Test
- void applyKpiTemplateAndLockUsesParticipantSnapshotPosition() {
- PeriodParticipant participant = participantRecord(7L, 12L, "ACTIVE");
- participant.setPositionSnapshot("研发工程师");
- when(participantMapper.selectOne(any())).thenReturn(participant);
- KpiTemplateAssignment assignment = new KpiTemplateAssignment();
- assignment.setTemplateId(5L);
- assignment.setTargetType("POSITION");
- assignment.setTargetValue("研发工程师");
- assignment.setPriority(10);
- when(kpiTemplateAssignmentMapper.selectList(any())).thenReturn(List.of(assignment));
- KpiTemplate template = new KpiTemplate();
- template.setId(5L);
- template.setItemsJson("[{\"name\":\"交付质量\",\"maxScore\":40}]");
- when(kpiTemplateMapper.selectById(5L)).thenReturn(template);
- when(kpiConfigMapper.selectOne(any())).thenReturn(null);
- KpiConfig config = periodService.applyKpiTemplateAndLock(7L, 12L);
- assertNotNull(config.getLockedAt());
- assertEquals(template.getItemsJson(), config.getItemsJson());
- verify(kpiConfigMapper).insert(argThat(value -> {
- KpiConfig inserted = (KpiConfig) value;
- return Long.valueOf(7L).equals(inserted.getPeriodId())
- && Long.valueOf(12L).equals(inserted.getUserId())
- && inserted.getLockedAt() != null;
- }));
- }
- @Test
- void applyKpiTemplateAndLockBackfillsMissingLegacyPositionSnapshot() {
- PeriodParticipant participant = participantRecord(7L, 12L, "ACTIVE");
- when(participantMapper.selectOne(any())).thenReturn(participant);
- SysUser user = participant(12L);
- user.setPosition("后端开发工程师");
- when(userMapper.selectById(12L)).thenReturn(user);
- KpiTemplateAssignment assignment = new KpiTemplateAssignment();
- assignment.setTemplateId(2L);
- assignment.setTargetType("POSITION");
- assignment.setTargetValue("后端开发工程师");
- assignment.setPriority(10);
- when(kpiTemplateAssignmentMapper.selectList(any())).thenReturn(List.of(assignment));
- KpiTemplate template = new KpiTemplate();
- template.setId(2L);
- template.setItemsJson("[{\"name\":\"交付质量\",\"maxScore\":40}]");
- when(kpiTemplateMapper.selectById(2L)).thenReturn(template);
- when(kpiConfigMapper.selectOne(any())).thenReturn(null);
- KpiConfig config = periodService.applyKpiTemplateAndLock(7L, 12L);
- assertNotNull(config.getLockedAt());
- assertEquals("后端开发工程师", participant.getPositionSnapshot());
- verify(participantMapper).updateById(participant);
- }
- @Test
- void addParticipantCreatesActiveParticipantWhenUserExists() {
- when(userMapper.selectById(12L)).thenReturn(participant(12L));
- when(participantMapper.selectOne(any())).thenReturn(null);
- PeriodParticipant participant = periodService.addParticipant(7L, 12L);
- assertEquals("ACTIVE", participant.getStatus());
- assertEquals(7L, participant.getPeriodId());
- assertEquals(12L, participant.getUserId());
- verify(participantMapper).insert(participant);
- }
- @Test
- void activateParticipantRevertsExemption() {
- PeriodParticipant participant = participantRecord(7L, 12L, "EXEMPT");
- participant.setReason("休假");
- when(participantMapper.selectOne(any())).thenReturn(participant);
- PeriodParticipant active = periodService.activateParticipant(7L, 12L);
- assertEquals("ACTIVE", active.getStatus());
- assertNull(active.getReason());
- assertNull(active.getExemptedAt());
- verify(participantMapper).updateById(participant);
- }
- @Test
- void batchCreateKeepsExplicitCompanyOwnerForEveryPeriod() {
- List<AssessmentPeriod> periods = periodService.batchCreate(Map.of(
- "namePrefix", "2026-Q",
- "periodType", "QUARTERLY",
- "startDate", "2026-01-01",
- "companyOwnerUserId", 99
- ));
- assertEquals(6, periods.size());
- assertTrue(periods.stream().allMatch(period -> Long.valueOf(99L).equals(period.getCompanyOwnerUserId())));
- verify(periodMapper, times(6)).insert(argThat(period ->
- Long.valueOf(99L).equals(((AssessmentPeriod) period).getCompanyOwnerUserId())));
- }
- @Test
- void createRejectsInvalidPeerReviewDeadlineOrder() {
- AssessmentPeriod period = period(null, PeriodStatus.DRAFT);
- period.setPeerInvitationDeadline(LocalDateTime.of(2026, 12, 20, 18, 0));
- period.setPeerReviewDeadline(LocalDateTime.of(2026, 12, 19, 18, 0));
- BusinessException error = assertThrows(BusinessException.class,
- () -> periodService.create(period));
- assertTrue(error.getMessage().contains("邀请名单截止时间必须早于评价提交截止时间"));
- verify(periodMapper, never()).insert(any());
- }
- @Test
- void assessingPeriodAllowsOnlyDeadlineExtension() {
- AssessmentPeriod existing = period(20L, PeriodStatus.ASSESSING);
- existing.setPeerInvitationDeadline(LocalDateTime.now().plusDays(1));
- existing.setPeerReviewDeadline(LocalDateTime.now().plusDays(2));
- when(periodMapper.selectById(20L)).thenReturn(existing);
- AssessmentPeriod shortened = period(20L, PeriodStatus.ASSESSING);
- shortened.setPeerInvitationDeadline(existing.getPeerInvitationDeadline().minusHours(1));
- shortened.setPeerReviewDeadline(existing.getPeerReviewDeadline());
- assertThrows(BusinessException.class, () -> periodService.update(shortened));
- AssessmentPeriod extended = period(20L, PeriodStatus.ASSESSING);
- extended.setPeerInvitationDeadline(existing.getPeerInvitationDeadline().plusHours(1));
- extended.setPeerReviewDeadline(existing.getPeerReviewDeadline().plusHours(1));
- assertDoesNotThrow(() -> periodService.update(extended));
- verify(periodMapper).updateById(argThat(item ->
- item.getPeerReviewDeadline().equals(extended.getPeerReviewDeadline())));
- }
- @Test
- void assessingLegacyPeriodCanConfigureMissingDeadlines() {
- AssessmentPeriod existing = period(21L, PeriodStatus.ASSESSING);
- when(periodMapper.selectById(21L)).thenReturn(existing);
- AssessmentPeriod update = period(21L, PeriodStatus.ASSESSING);
- update.setPeerInvitationDeadline(LocalDateTime.now().plusDays(1));
- update.setPeerReviewDeadline(LocalDateTime.now().plusDays(2));
- assertDoesNotThrow(() -> periodService.update(update));
- verify(periodMapper).updateById(argThat(item ->
- item.getPeerInvitationDeadline().equals(update.getPeerInvitationDeadline())));
- }
- private AssessmentPeriod period(Long id, PeriodStatus status) {
- AssessmentPeriod period = new AssessmentPeriod();
- period.setId(id);
- period.setName("2026年6月");
- period.setStatus(status);
- return period;
- }
- private SysUser participant(Long id) {
- SysUser user = new SysUser();
- user.setId(id);
- user.setUsername("u" + id);
- user.setRealName("员工" + id);
- user.setDepartmentId(1L);
- user.setSuperiorId(99L);
- user.setRole(UserRole.EMPLOYEE);
- return user;
- }
- private PeriodParticipant participantRecord(Long periodId, Long userId, String status) {
- PeriodParticipant participant = new PeriodParticipant();
- participant.setPeriodId(periodId);
- participant.setUserId(userId);
- participant.setStatus(status);
- return participant;
- }
- private KpiConfig lockedKpi(Long userId) {
- KpiConfig config = new KpiConfig();
- config.setUserId(userId);
- config.setLockedAt(java.time.LocalDateTime.now());
- return config;
- }
- private Object readField(Object target, String fieldName) {
- try {
- var field = target.getClass().getDeclaredField(fieldName);
- field.setAccessible(true);
- return field.get(target);
- } catch (ReflectiveOperationException ignored) {
- return null;
- }
- }
- }
|