AssessmentPeriodServiceTest.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. package com.emoon.okr.service;
  2. import com.emoon.okr.entity.KpiConfig;
  3. import com.emoon.okr.entity.KpiTemplate;
  4. import com.emoon.okr.entity.KpiTemplateAssignment;
  5. import com.emoon.okr.entity.OkrObjective;
  6. import com.emoon.okr.entity.AssessmentPeriod;
  7. import com.emoon.okr.entity.PeriodParticipant;
  8. import com.emoon.okr.entity.SysUser;
  9. import com.emoon.okr.enums.PeriodStatus;
  10. import com.emoon.okr.enums.ScoreType;
  11. import com.emoon.okr.enums.UserRole;
  12. import com.emoon.okr.exception.BusinessException;
  13. import com.emoon.okr.mapper.*;
  14. import org.junit.jupiter.api.Test;
  15. import org.junit.jupiter.api.BeforeEach;
  16. import org.junit.jupiter.api.extension.ExtendWith;
  17. import org.mockito.InjectMocks;
  18. import org.mockito.Mock;
  19. import org.mockito.junit.jupiter.MockitoExtension;
  20. import org.springframework.context.ApplicationEventPublisher;
  21. import java.util.List;
  22. import java.util.ArrayList;
  23. import java.util.Map;
  24. import java.time.LocalDateTime;
  25. import static org.junit.jupiter.api.Assertions.*;
  26. import static org.mockito.ArgumentMatchers.any;
  27. import static org.mockito.Mockito.*;
  28. @ExtendWith(MockitoExtension.class)
  29. class AssessmentPeriodServiceTest {
  30. @Mock AssessmentPeriodMapper periodMapper;
  31. @Mock SysUserMapper userMapper;
  32. @Mock OkrObjectiveMapper objectiveMapper;
  33. @Mock KrKeyResultMapper krMapper;
  34. @Mock KpiConfigMapper kpiConfigMapper;
  35. @Mock KpiTemplateMapper kpiTemplateMapper;
  36. @Mock KpiTemplateAssignmentMapper kpiTemplateAssignmentMapper;
  37. @Mock PerformanceScoreMapper scoreMapper;
  38. @Mock PerformanceFeedbackMapper feedbackMapper;
  39. @Mock PeriodParticipantMapper participantMapper;
  40. @Mock AssessmentDimensionMapper dimensionMapper;
  41. @Mock PeriodDimensionMapper periodDimensionMapper;
  42. @Mock ScoreAppealMapper appealMapper;
  43. @Mock ApplicationEventPublisher publisher;
  44. @InjectMocks AssessmentPeriodService periodService;
  45. @BeforeEach
  46. void defaultFortyPointDimensions() {
  47. var dimensions = new ArrayList<com.emoon.okr.entity.AssessmentDimension>();
  48. for (int i = 0; i < 4; i++) {
  49. var dimension = new com.emoon.okr.entity.AssessmentDimension();
  50. dimension.setId((long) i + 1);
  51. dimension.setName("维度" + i);
  52. dimension.setMaxScore(10);
  53. dimension.setSortOrder(i);
  54. dimensions.add(dimension);
  55. }
  56. lenient().when(dimensionMapper.selectList(any())).thenReturn(dimensions);
  57. }
  58. @Test
  59. void transitionToExecutingRejectsPendingOkrReview() {
  60. AssessmentPeriod period = period(1L, PeriodStatus.OKR_ALIGN);
  61. when(periodMapper.selectById(1L)).thenReturn(period);
  62. when(objectiveMapper.selectCount(any())).thenReturn(1L);
  63. BusinessException ex = assertThrows(BusinessException.class,
  64. () -> periodService.transitionStatus(1L, PeriodStatus.EXECUTING));
  65. assertTrue(ex.getMessage().contains("待审核OKR"));
  66. verify(periodMapper, never()).updateById(any());
  67. }
  68. @Test
  69. void transitionToArchivedRejectsUnconfirmedFinalScores() {
  70. AssessmentPeriod period = period(2L, PeriodStatus.ASSESSING);
  71. when(periodMapper.selectById(2L)).thenReturn(period);
  72. when(scoreMapper.selectCount(any())).thenReturn(1L, 1L);
  73. BusinessException ex = assertThrows(BusinessException.class,
  74. () -> periodService.transitionStatus(2L, PeriodStatus.ARCHIVED));
  75. assertTrue(ex.getMessage().contains("未确认最终评分"));
  76. verify(periodMapper, never()).updateById(any());
  77. }
  78. @Test
  79. void transitionFromExecutingCannotSkipAssessmentAndArchiveDirectly() {
  80. AssessmentPeriod period = period(9L, PeriodStatus.EXECUTING);
  81. when(periodMapper.selectById(9L)).thenReturn(period);
  82. BusinessException ex = assertThrows(BusinessException.class,
  83. () -> periodService.transitionStatus(9L, PeriodStatus.ARCHIVED));
  84. assertTrue(ex.getMessage().contains("进入考评"));
  85. verify(periodMapper, never()).updateById(any());
  86. }
  87. @Test
  88. void transitionToExecutingRejectsParticipantsWithoutApprovedOkr() {
  89. AssessmentPeriod period = period(3L, PeriodStatus.OKR_ALIGN);
  90. when(periodMapper.selectById(3L)).thenReturn(period);
  91. when(objectiveMapper.selectCount(any())).thenReturn(0L);
  92. when(kpiConfigMapper.selectCount(any())).thenReturn(0L);
  93. OkrObjective approved = new OkrObjective();
  94. approved.setUserId(10L);
  95. approved.setStatus("APPROVED");
  96. when(objectiveMapper.selectList(any())).thenReturn(List.of(approved));
  97. KpiConfig locked10 = lockedKpi(10L);
  98. KpiConfig locked11 = lockedKpi(11L);
  99. when(kpiConfigMapper.selectList(any())).thenReturn(List.of(locked10, locked11));
  100. BusinessException ex = assertThrows(BusinessException.class,
  101. () -> periodService.transitionStatus(3L, PeriodStatus.EXECUTING));
  102. assertTrue(ex.getMessage().contains("未提交或未通过OKR"));
  103. verify(periodMapper, never()).updateById(any());
  104. }
  105. @Test
  106. void transitionToArchivedRejectsMissingSelfScoreAndFeedbackReply() {
  107. AssessmentPeriod period = period(4L, PeriodStatus.ASSESSING);
  108. when(periodMapper.selectById(4L)).thenReturn(period);
  109. when(scoreMapper.selectCount(any())).thenReturn(1L, 0L);
  110. when(kpiConfigMapper.selectList(any())).thenReturn(List.of(lockedKpi(10L)));
  111. BusinessException ex = assertThrows(BusinessException.class,
  112. () -> periodService.transitionStatus(4L, PeriodStatus.ARCHIVED));
  113. assertTrue(ex.getMessage().contains("未完成自评"));
  114. verify(periodMapper, never()).updateById(any());
  115. }
  116. @Test
  117. void readinessUsesOnlyUsersWithPeriodDataAsParticipants() {
  118. AssessmentPeriod period = period(5L, PeriodStatus.OKR_ALIGN);
  119. when(periodMapper.selectById(5L)).thenReturn(period);
  120. when(objectiveMapper.selectCount(any())).thenReturn(0L);
  121. when(kpiConfigMapper.selectCount(any())).thenReturn(0L);
  122. OkrObjective approved = new OkrObjective();
  123. approved.setUserId(10L);
  124. approved.setStatus("APPROVED");
  125. when(objectiveMapper.selectList(any())).thenReturn(List.of(approved));
  126. when(kpiConfigMapper.selectList(any())).thenReturn(List.of(lockedKpi(10L)));
  127. var readiness = periodService.checkReadiness(5L, PeriodStatus.EXECUTING);
  128. assertTrue(readiness.isReady());
  129. assertTrue(readiness.getBlockers().isEmpty());
  130. verify(userMapper, never()).selectList(any());
  131. }
  132. @Test
  133. void transitionToOkrAlignCreatesActiveParticipantSnapshotForActiveDepartmentUsers() {
  134. AssessmentPeriod period = period(6L, PeriodStatus.DRAFT);
  135. when(periodMapper.selectById(6L)).thenReturn(period);
  136. SysUser active = participant(10L);
  137. active.setStatus("ACTIVE");
  138. SysUser disabled = participant(11L);
  139. disabled.setStatus("DISABLED");
  140. SysUser ceo = participant(99L);
  141. ceo.setSuperiorId(null);
  142. ceo.setStatus("ACTIVE");
  143. when(userMapper.selectList(any())).thenReturn(List.of(ceo, active, disabled));
  144. periodService.transitionStatus(6L, PeriodStatus.OKR_ALIGN);
  145. verify(participantMapper).insert(argThat(p ->
  146. p instanceof PeriodParticipant
  147. && ((PeriodParticipant) p).getPeriodId().equals(6L)
  148. && ((PeriodParticipant) p).getUserId().equals(10L)
  149. && "ACTIVE".equals(((PeriodParticipant) p).getStatus())));
  150. verify(participantMapper, never()).insert(argThat(p ->
  151. p instanceof PeriodParticipant && ((PeriodParticipant) p).getUserId().equals(11L)));
  152. }
  153. @Test
  154. void transitionToOkrAlignFreezesReportingLineAndExemptsUserWithoutEvaluator() {
  155. AssessmentPeriod period = period(8L, PeriodStatus.DRAFT);
  156. when(periodMapper.selectById(8L)).thenReturn(period);
  157. SysUser ceo = participant(20L);
  158. ceo.setPosition("CEO");
  159. ceo.setStatus("ACTIVE");
  160. ceo.setSuperiorId(null);
  161. SysUser employee = participant(21L);
  162. employee.setPosition("研发工程师");
  163. employee.setStatus("ACTIVE");
  164. employee.setSuperiorId(20L);
  165. when(userMapper.selectList(any())).thenReturn(List.of(ceo, employee));
  166. List<PeriodParticipant> inserted = new ArrayList<>();
  167. doAnswer(invocation -> {
  168. inserted.add(invocation.getArgument(0));
  169. return 1;
  170. }).when(participantMapper).insert(any());
  171. periodService.transitionStatus(8L, PeriodStatus.OKR_ALIGN);
  172. PeriodParticipant ceoSnapshot = inserted.stream()
  173. .filter(p -> p.getUserId().equals(20L)).findFirst().orElseThrow();
  174. PeriodParticipant employeeSnapshot = inserted.stream()
  175. .filter(p -> p.getUserId().equals(21L)).findFirst().orElseThrow();
  176. assertEquals("EXEMPT_NO_EVALUATOR", ceoSnapshot.getStatus());
  177. assertEquals("ACTIVE", employeeSnapshot.getStatus());
  178. assertEquals(20L, readField(employeeSnapshot, "superiorIdSnapshot"));
  179. assertEquals(20L, readField(employeeSnapshot, "evaluatorId"));
  180. assertEquals(1L, readField(employeeSnapshot, "departmentIdSnapshot"));
  181. assertEquals("研发工程师", readField(employeeSnapshot, "positionSnapshot"));
  182. assertEquals("EMPLOYEE", readField(employeeSnapshot, "roleSnapshot"));
  183. }
  184. @Test
  185. void transitionToOkrAlignInstantiatesAndLocksExplicitlyAssignedPerformanceTemplate() {
  186. AssessmentPeriod period = period(10L, PeriodStatus.DRAFT);
  187. period.setCompanyOwnerUserId(20L);
  188. when(periodMapper.selectById(10L)).thenReturn(period);
  189. SysUser ceo = participant(20L);
  190. ceo.setSuperiorId(null);
  191. ceo.setStatus("ACTIVE");
  192. ceo.setPosition("CEO");
  193. SysUser employee = participant(21L);
  194. employee.setSuperiorId(20L);
  195. employee.setStatus("ACTIVE");
  196. employee.setPosition("研发工程师");
  197. when(userMapper.selectList(any())).thenReturn(List.of(ceo, employee));
  198. PeriodParticipant assignedEmployee = participantRecord(10L, 21L, "ACTIVE");
  199. assignedEmployee.setDepartmentIdSnapshot(employee.getDepartmentId());
  200. assignedEmployee.setPositionSnapshot(employee.getPosition());
  201. when(participantMapper.selectList(any())).thenReturn(List.of(assignedEmployee));
  202. KpiTemplateAssignment assignment = new KpiTemplateAssignment();
  203. assignment.setTemplateId(5L);
  204. assignment.setTargetType("POSITION");
  205. assignment.setTargetValue("研发工程师");
  206. assignment.setPriority(10);
  207. when(kpiTemplateAssignmentMapper.selectList(any())).thenReturn(List.of(assignment));
  208. KpiTemplate template = new KpiTemplate();
  209. template.setId(5L);
  210. template.setItemsJson("[{\"name\":\"交付质量\",\"maxScore\":40}]");
  211. when(kpiTemplateMapper.selectById(5L)).thenReturn(template);
  212. periodService.transitionStatus(10L, PeriodStatus.OKR_ALIGN);
  213. verify(kpiConfigMapper).insert(argThat(config -> {
  214. KpiConfig value = (KpiConfig) config;
  215. return Long.valueOf(10L).equals(value.getPeriodId())
  216. && Long.valueOf(21L).equals(value.getUserId())
  217. && value.getLockedAt() != null
  218. && template.getItemsJson().equals(value.getItemsJson());
  219. }));
  220. }
  221. @Test
  222. void readinessIgnoresExemptParticipant() {
  223. AssessmentPeriod period = period(7L, PeriodStatus.OKR_ALIGN);
  224. when(periodMapper.selectById(7L)).thenReturn(period);
  225. when(objectiveMapper.selectCount(any())).thenReturn(0L);
  226. when(kpiConfigMapper.selectCount(any())).thenReturn(0L);
  227. PeriodParticipant active = participantRecord(7L, 10L, "ACTIVE");
  228. PeriodParticipant exempt = participantRecord(7L, 11L, "EXEMPT");
  229. when(participantMapper.selectList(any())).thenReturn(List.of(active, exempt));
  230. OkrObjective approved = new OkrObjective();
  231. approved.setUserId(10L);
  232. approved.setStatus("APPROVED");
  233. when(objectiveMapper.selectList(any())).thenReturn(List.of(approved));
  234. when(kpiConfigMapper.selectList(any())).thenReturn(List.of(lockedKpi(10L)));
  235. var readiness = periodService.checkReadiness(7L, PeriodStatus.EXECUTING);
  236. assertTrue(readiness.isReady());
  237. }
  238. @Test
  239. void applyKpiTemplateAndLockUsesParticipantSnapshotPosition() {
  240. PeriodParticipant participant = participantRecord(7L, 12L, "ACTIVE");
  241. participant.setPositionSnapshot("研发工程师");
  242. when(participantMapper.selectOne(any())).thenReturn(participant);
  243. KpiTemplateAssignment assignment = new KpiTemplateAssignment();
  244. assignment.setTemplateId(5L);
  245. assignment.setTargetType("POSITION");
  246. assignment.setTargetValue("研发工程师");
  247. assignment.setPriority(10);
  248. when(kpiTemplateAssignmentMapper.selectList(any())).thenReturn(List.of(assignment));
  249. KpiTemplate template = new KpiTemplate();
  250. template.setId(5L);
  251. template.setItemsJson("[{\"name\":\"交付质量\",\"maxScore\":40}]");
  252. when(kpiTemplateMapper.selectById(5L)).thenReturn(template);
  253. when(kpiConfigMapper.selectOne(any())).thenReturn(null);
  254. KpiConfig config = periodService.applyKpiTemplateAndLock(7L, 12L);
  255. assertNotNull(config.getLockedAt());
  256. assertEquals(template.getItemsJson(), config.getItemsJson());
  257. verify(kpiConfigMapper).insert(argThat(value -> {
  258. KpiConfig inserted = (KpiConfig) value;
  259. return Long.valueOf(7L).equals(inserted.getPeriodId())
  260. && Long.valueOf(12L).equals(inserted.getUserId())
  261. && inserted.getLockedAt() != null;
  262. }));
  263. }
  264. @Test
  265. void applyKpiTemplateAndLockBackfillsMissingLegacyPositionSnapshot() {
  266. PeriodParticipant participant = participantRecord(7L, 12L, "ACTIVE");
  267. when(participantMapper.selectOne(any())).thenReturn(participant);
  268. SysUser user = participant(12L);
  269. user.setPosition("后端开发工程师");
  270. when(userMapper.selectById(12L)).thenReturn(user);
  271. KpiTemplateAssignment assignment = new KpiTemplateAssignment();
  272. assignment.setTemplateId(2L);
  273. assignment.setTargetType("POSITION");
  274. assignment.setTargetValue("后端开发工程师");
  275. assignment.setPriority(10);
  276. when(kpiTemplateAssignmentMapper.selectList(any())).thenReturn(List.of(assignment));
  277. KpiTemplate template = new KpiTemplate();
  278. template.setId(2L);
  279. template.setItemsJson("[{\"name\":\"交付质量\",\"maxScore\":40}]");
  280. when(kpiTemplateMapper.selectById(2L)).thenReturn(template);
  281. when(kpiConfigMapper.selectOne(any())).thenReturn(null);
  282. KpiConfig config = periodService.applyKpiTemplateAndLock(7L, 12L);
  283. assertNotNull(config.getLockedAt());
  284. assertEquals("后端开发工程师", participant.getPositionSnapshot());
  285. verify(participantMapper).updateById(participant);
  286. }
  287. @Test
  288. void addParticipantCreatesActiveParticipantWhenUserExists() {
  289. when(userMapper.selectById(12L)).thenReturn(participant(12L));
  290. when(participantMapper.selectOne(any())).thenReturn(null);
  291. PeriodParticipant participant = periodService.addParticipant(7L, 12L);
  292. assertEquals("ACTIVE", participant.getStatus());
  293. assertEquals(7L, participant.getPeriodId());
  294. assertEquals(12L, participant.getUserId());
  295. verify(participantMapper).insert(participant);
  296. }
  297. @Test
  298. void activateParticipantRevertsExemption() {
  299. PeriodParticipant participant = participantRecord(7L, 12L, "EXEMPT");
  300. participant.setReason("休假");
  301. when(participantMapper.selectOne(any())).thenReturn(participant);
  302. PeriodParticipant active = periodService.activateParticipant(7L, 12L);
  303. assertEquals("ACTIVE", active.getStatus());
  304. assertNull(active.getReason());
  305. assertNull(active.getExemptedAt());
  306. verify(participantMapper).updateById(participant);
  307. }
  308. @Test
  309. void batchCreateKeepsExplicitCompanyOwnerForEveryPeriod() {
  310. List<AssessmentPeriod> periods = periodService.batchCreate(Map.of(
  311. "namePrefix", "2026-Q",
  312. "periodType", "QUARTERLY",
  313. "startDate", "2026-01-01",
  314. "companyOwnerUserId", 99
  315. ));
  316. assertEquals(6, periods.size());
  317. assertTrue(periods.stream().allMatch(period -> Long.valueOf(99L).equals(period.getCompanyOwnerUserId())));
  318. verify(periodMapper, times(6)).insert(argThat(period ->
  319. Long.valueOf(99L).equals(((AssessmentPeriod) period).getCompanyOwnerUserId())));
  320. }
  321. @Test
  322. void createRejectsInvalidPeerReviewDeadlineOrder() {
  323. AssessmentPeriod period = period(null, PeriodStatus.DRAFT);
  324. period.setPeerInvitationDeadline(LocalDateTime.of(2026, 12, 20, 18, 0));
  325. period.setPeerReviewDeadline(LocalDateTime.of(2026, 12, 19, 18, 0));
  326. BusinessException error = assertThrows(BusinessException.class,
  327. () -> periodService.create(period));
  328. assertTrue(error.getMessage().contains("邀请名单截止时间必须早于评价提交截止时间"));
  329. verify(periodMapper, never()).insert(any());
  330. }
  331. @Test
  332. void assessingPeriodAllowsOnlyDeadlineExtension() {
  333. AssessmentPeriod existing = period(20L, PeriodStatus.ASSESSING);
  334. existing.setPeerInvitationDeadline(LocalDateTime.now().plusDays(1));
  335. existing.setPeerReviewDeadline(LocalDateTime.now().plusDays(2));
  336. when(periodMapper.selectById(20L)).thenReturn(existing);
  337. AssessmentPeriod shortened = period(20L, PeriodStatus.ASSESSING);
  338. shortened.setPeerInvitationDeadline(existing.getPeerInvitationDeadline().minusHours(1));
  339. shortened.setPeerReviewDeadline(existing.getPeerReviewDeadline());
  340. assertThrows(BusinessException.class, () -> periodService.update(shortened));
  341. AssessmentPeriod extended = period(20L, PeriodStatus.ASSESSING);
  342. extended.setPeerInvitationDeadline(existing.getPeerInvitationDeadline().plusHours(1));
  343. extended.setPeerReviewDeadline(existing.getPeerReviewDeadline().plusHours(1));
  344. assertDoesNotThrow(() -> periodService.update(extended));
  345. verify(periodMapper).updateById(argThat(item ->
  346. item.getPeerReviewDeadline().equals(extended.getPeerReviewDeadline())));
  347. }
  348. @Test
  349. void assessingLegacyPeriodCanConfigureMissingDeadlines() {
  350. AssessmentPeriod existing = period(21L, PeriodStatus.ASSESSING);
  351. when(periodMapper.selectById(21L)).thenReturn(existing);
  352. AssessmentPeriod update = period(21L, PeriodStatus.ASSESSING);
  353. update.setPeerInvitationDeadline(LocalDateTime.now().plusDays(1));
  354. update.setPeerReviewDeadline(LocalDateTime.now().plusDays(2));
  355. assertDoesNotThrow(() -> periodService.update(update));
  356. verify(periodMapper).updateById(argThat(item ->
  357. item.getPeerInvitationDeadline().equals(update.getPeerInvitationDeadline())));
  358. }
  359. private AssessmentPeriod period(Long id, PeriodStatus status) {
  360. AssessmentPeriod period = new AssessmentPeriod();
  361. period.setId(id);
  362. period.setName("2026年6月");
  363. period.setStatus(status);
  364. return period;
  365. }
  366. private SysUser participant(Long id) {
  367. SysUser user = new SysUser();
  368. user.setId(id);
  369. user.setUsername("u" + id);
  370. user.setRealName("员工" + id);
  371. user.setDepartmentId(1L);
  372. user.setSuperiorId(99L);
  373. user.setRole(UserRole.EMPLOYEE);
  374. return user;
  375. }
  376. private PeriodParticipant participantRecord(Long periodId, Long userId, String status) {
  377. PeriodParticipant participant = new PeriodParticipant();
  378. participant.setPeriodId(periodId);
  379. participant.setUserId(userId);
  380. participant.setStatus(status);
  381. return participant;
  382. }
  383. private KpiConfig lockedKpi(Long userId) {
  384. KpiConfig config = new KpiConfig();
  385. config.setUserId(userId);
  386. config.setLockedAt(java.time.LocalDateTime.now());
  387. return config;
  388. }
  389. private Object readField(Object target, String fieldName) {
  390. try {
  391. var field = target.getClass().getDeclaredField(fieldName);
  392. field.setAccessible(true);
  393. return field.get(target);
  394. } catch (ReflectiveOperationException ignored) {
  395. return null;
  396. }
  397. }
  398. }