|
|
@@ -0,0 +1,596 @@
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
+import com.etotem.cfc.controller.assessment.AssessmentAppointmentController;
|
|
|
+import com.etotem.cfc.controller.assessment.AssessmentQuotaController;
|
|
|
+import com.etotem.cfc.entity.AssessmentAppointment;
|
|
|
+import com.etotem.cfc.entity.AssessmentQuota;
|
|
|
+import com.etotem.cfc.entity.DanAssessmentExecution;
|
|
|
+import com.etotem.cfc.entity.ProductOrder;
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
+import com.etotem.cfc.mapper.AssessmentAppointmentMapper;
|
|
|
+import com.etotem.cfc.mapper.AssessmentQuotaMapper;
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
+import com.etotem.cfc.service.AssessmentAppointmentService;
|
|
|
+import com.etotem.cfc.service.AssessmentExecutionService;
|
|
|
+import com.etotem.cfc.service.AssessmentQuotaService;
|
|
|
+import com.etotem.cfc.service.DanAssessmentExecutionService;
|
|
|
+import com.etotem.cfc.service.EnergyService;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.ArgumentCaptor;
|
|
|
+
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.ArgumentMatchers.anyLong;
|
|
|
+import static org.mockito.ArgumentMatchers.eq;
|
|
|
+import static org.mockito.ArgumentMatchers.isNull;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * EVD-8 契约补建 · 测评-方案「测评额度/预约」批次三类流契约测试。
|
|
|
+ *
|
|
|
+ * <p>覆盖 SC(contract-plan 测评-方案):SC-P-33(套餐下单→额度发放)、SC-P-35(获得测评额度)、
|
|
|
+ * SC-P-36(创建预约消耗额度)、SC-P-37(查看预约状态)、SC-P-38(取消预约·越权观察)、
|
|
|
+ * SC-T-09(预约确认状态守卫)、SC-T-10(预约完成守卫)、SC-X-01(测评全链路·预约+执行记录)。</p>
|
|
|
+ *
|
|
|
+ * <p>三类流(纯 Mockito / 无 Spring / 无 DB):正常流、异常流(额度不足/过期/已用完/状态异常)、
|
|
|
+ * 权限流(非指派确认、越权取消观察、guide-list 角色守卫)。</p>
|
|
|
+ */
|
|
|
+class AssessmentQuotaAppointmentContractTest extends AssessmentPlanContractTestBase {
|
|
|
+
|
|
|
+ // ==================== service:AssessmentQuotaService ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-P-36 正常:消耗一次额度 remaining 3→2")
|
|
|
+ void quota_正常_消耗一次() throws Exception {
|
|
|
+ AssessmentQuotaMapper mapper = mock(AssessmentQuotaMapper.class);
|
|
|
+ AssessmentQuotaService svc = new AssessmentQuotaService();
|
|
|
+ inject(svc, "mapper", mapper);
|
|
|
+
|
|
|
+ AssessmentQuota quota = quota(5L, "active", 3, 3, future());
|
|
|
+ when(mapper.selectById(5L)).thenReturn(quota);
|
|
|
+
|
|
|
+ int remaining = svc.consumeQuota(5L, 10L);
|
|
|
+ assertEquals(2, remaining);
|
|
|
+ assertEquals(2, quota.getRemainingSessions());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-P-36 异常:最后一格消耗后状态 used_all")
|
|
|
+ void quota_异常_最后一格_used_all() throws Exception {
|
|
|
+ AssessmentQuotaMapper mapper = mock(AssessmentQuotaMapper.class);
|
|
|
+ AssessmentQuotaService svc = new AssessmentQuotaService();
|
|
|
+ inject(svc, "mapper", mapper);
|
|
|
+
|
|
|
+ AssessmentQuota quota = quota(5L, "active", 1, 1, future());
|
|
|
+ when(mapper.selectById(5L)).thenReturn(quota);
|
|
|
+
|
|
|
+ int remaining = svc.consumeQuota(5L, 10L);
|
|
|
+ assertEquals(0, remaining);
|
|
|
+ assertEquals("used_all", quota.getStatus());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-P-36 异常:额度已用完 → 抛「测评额度已用完」")
|
|
|
+ void quota_异常_已用完() throws Exception {
|
|
|
+ AssessmentQuotaMapper mapper = mock(AssessmentQuotaMapper.class);
|
|
|
+ AssessmentQuotaService svc = new AssessmentQuotaService();
|
|
|
+ inject(svc, "mapper", mapper);
|
|
|
+
|
|
|
+ when(mapper.selectById(5L)).thenReturn(quota(5L, "active", 0, 0, future()));
|
|
|
+ RuntimeException ex = assertThrows(RuntimeException.class, () -> svc.consumeQuota(5L, 10L));
|
|
|
+ assertTrue(ex.getMessage().contains("已用完"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-P-36 异常:额度已过期 → 置 expired 并抛「测评额度已过期」")
|
|
|
+ void quota_异常_已过期() throws Exception {
|
|
|
+ AssessmentQuotaMapper mapper = mock(AssessmentQuotaMapper.class);
|
|
|
+ AssessmentQuotaService svc = new AssessmentQuotaService();
|
|
|
+ inject(svc, "mapper", mapper);
|
|
|
+
|
|
|
+ AssessmentQuota quota = quota(5L, "active", 2, 2, past());
|
|
|
+ when(mapper.selectById(5L)).thenReturn(quota);
|
|
|
+
|
|
|
+ RuntimeException ex = assertThrows(RuntimeException.class, () -> svc.consumeQuota(5L, 10L));
|
|
|
+ assertTrue(ex.getMessage().contains("已过期"));
|
|
|
+ assertEquals("expired", quota.getStatus());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-P-36 异常:状态非 active → 抛「测评额度状态异常」")
|
|
|
+ void quota_异常_状态异常() throws Exception {
|
|
|
+ AssessmentQuotaMapper mapper = mock(AssessmentQuotaMapper.class);
|
|
|
+ AssessmentQuotaService svc = new AssessmentQuotaService();
|
|
|
+ inject(svc, "mapper", mapper);
|
|
|
+
|
|
|
+ when(mapper.selectById(5L)).thenReturn(quota(5L, "used_all", 0, 0, future()));
|
|
|
+ RuntimeException ex = assertThrows(RuntimeException.class, () -> svc.consumeQuota(5L, 10L));
|
|
|
+ assertTrue(ex.getMessage().contains("状态异常"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-P-36 异常:额度记录不存在 → 抛「测评额度不存在」")
|
|
|
+ void quota_异常_不存在() throws Exception {
|
|
|
+ AssessmentQuotaMapper mapper = mock(AssessmentQuotaMapper.class);
|
|
|
+ AssessmentQuotaService svc = new AssessmentQuotaService();
|
|
|
+ inject(svc, "mapper", mapper);
|
|
|
+
|
|
|
+ when(mapper.selectById(5L)).thenReturn(null);
|
|
|
+ RuntimeException ex = assertThrows(RuntimeException.class, () -> svc.consumeQuota(5L, 10L));
|
|
|
+ assertTrue(ex.getMessage().contains("不存在"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-P-33/35 正常:套餐下单支付后发放额度(默认 1 次 / 365 天)")
|
|
|
+ void quota_正常_grantQuota默认值() throws Exception {
|
|
|
+ AssessmentQuotaMapper mapper = mock(AssessmentQuotaMapper.class);
|
|
|
+ AssessmentQuotaService svc = new AssessmentQuotaService();
|
|
|
+ inject(svc, "mapper", mapper);
|
|
|
+
|
|
|
+ ProductOrder order = mock(ProductOrder.class);
|
|
|
+ when(order.getId()).thenReturn(88L);
|
|
|
+ when(order.getFamilyId()).thenReturn(7L);
|
|
|
+ when(order.getProductId()).thenReturn(900L);
|
|
|
+ when(order.getProductName()).thenReturn("DAN测评套餐");
|
|
|
+ when(order.getOrderNo()).thenReturn("O888");
|
|
|
+
|
|
|
+ svc.grantQuota(order, null, null);
|
|
|
+
|
|
|
+ ArgumentCaptor<AssessmentQuota> captor = ArgumentCaptor.forClass(AssessmentQuota.class);
|
|
|
+ verify(mapper).insert(captor.capture());
|
|
|
+ AssessmentQuota q = captor.getValue();
|
|
|
+ assertEquals("active", q.getStatus());
|
|
|
+ assertEquals(1, q.getTotalSessions());
|
|
|
+ assertEquals(1, q.getRemainingSessions());
|
|
|
+ assertNotNull(q.getValidityEnd());
|
|
|
+ assertTrue(q.getValidityEnd().after(new Date()), "默认 365 天有效期应落在未来");
|
|
|
+ assertTrue(q.getQuotaNo().startsWith("Q"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== controller:AssessmentQuotaController ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-P-35 正常:我的可用额度列表 → VO 含 usedSessions/expired")
|
|
|
+ void quotaController_正常_我的额度() throws Exception {
|
|
|
+ AssessmentQuotaService quotaSvc = mock(AssessmentQuotaService.class);
|
|
|
+ AssessmentExecutionService execSvc = mock(AssessmentExecutionService.class);
|
|
|
+ UserMapper userMapper = mock(UserMapper.class);
|
|
|
+ AssessmentQuotaController c = new AssessmentQuotaController();
|
|
|
+ inject(c, "quotaService", quotaSvc);
|
|
|
+ inject(c, "executionService", execSvc);
|
|
|
+ inject(c, "userMapper", userMapper);
|
|
|
+
|
|
|
+ User user = new User();
|
|
|
+ user.setId(1L);
|
|
|
+ user.setFamilyId(7L);
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(user);
|
|
|
+ when(quotaSvc.getAvailableQuotas(7L, null))
|
|
|
+ .thenReturn(Arrays.asList(quota(5L, "active", 3, 2, future()), quota(6L, "active", 1, 1, future())));
|
|
|
+
|
|
|
+ Result<List<com.etotem.cfc.dto.AssessmentQuotaVO>> r = c.getMyQuotas(1L);
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
+ assertEquals(2, r.getData().size());
|
|
|
+ assertEquals(1, r.getData().get(0).getUsedSessions(), "3 次中已用 1 次");
|
|
|
+ assertEquals(false, r.getData().get(0).isExpired());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-P-35 权限:用户不存在/无家庭 → 业务提示")
|
|
|
+ void quotaController_权限_用户无家庭() throws Exception {
|
|
|
+ AssessmentQuotaService quotaSvc = mock(AssessmentQuotaService.class);
|
|
|
+ AssessmentExecutionService execSvc = mock(AssessmentExecutionService.class);
|
|
|
+ UserMapper userMapper = mock(UserMapper.class);
|
|
|
+ AssessmentQuotaController c = new AssessmentQuotaController();
|
|
|
+ inject(c, "quotaService", quotaSvc);
|
|
|
+ inject(c, "executionService", execSvc);
|
|
|
+ inject(c, "userMapper", userMapper);
|
|
|
+
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(null);
|
|
|
+ Result<?> r = c.getMyQuotas(1L);
|
|
|
+ assertEquals(500, r.getCode());
|
|
|
+ assertTrue(r.getMessage().contains("用户或家庭信息不存在"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-P-35 正常:额度详情 → VO")
|
|
|
+ void quotaController_正常_详情() throws Exception {
|
|
|
+ AssessmentQuotaService quotaSvc = mock(AssessmentQuotaService.class);
|
|
|
+ AssessmentExecutionService execSvc = mock(AssessmentExecutionService.class);
|
|
|
+ UserMapper userMapper = mock(UserMapper.class);
|
|
|
+ AssessmentQuotaController c = new AssessmentQuotaController();
|
|
|
+ inject(c, "quotaService", quotaSvc);
|
|
|
+ inject(c, "executionService", execSvc);
|
|
|
+ inject(c, "userMapper", userMapper);
|
|
|
+
|
|
|
+ when(quotaSvc.getById(5L)).thenReturn(quota(5L, "active", 3, 2, future()));
|
|
|
+ Map<String, Object> body = new HashMap<>();
|
|
|
+ body.put("quotaId", 5L);
|
|
|
+ Result<?> r = c.detail(body);
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
+ assertEquals(2, ((com.etotem.cfc.dto.AssessmentQuotaVO) r.getData()).getRemainingSessions());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-P-35 异常:quotaId 为空 → 业务提示")
|
|
|
+ void quotaController_异常_缺quotaId() throws Exception {
|
|
|
+ AssessmentQuotaService quotaSvc = mock(AssessmentQuotaService.class);
|
|
|
+ AssessmentQuotaController c = new AssessmentQuotaController();
|
|
|
+ inject(c, "quotaService", quotaSvc);
|
|
|
+ inject(c, "executionService", mock(AssessmentExecutionService.class));
|
|
|
+ inject(c, "userMapper", mock(UserMapper.class));
|
|
|
+
|
|
|
+ Result<?> r = c.detail(new HashMap<>());
|
|
|
+ assertEquals(500, r.getCode());
|
|
|
+ assertTrue(r.getMessage().contains("quotaId不能为空"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-P-35 异常:额度不存在 → 业务提示")
|
|
|
+ void quotaController_异常_额度不存在() throws Exception {
|
|
|
+ AssessmentQuotaService quotaSvc = mock(AssessmentQuotaService.class);
|
|
|
+ AssessmentQuotaController c = new AssessmentQuotaController();
|
|
|
+ inject(c, "quotaService", quotaSvc);
|
|
|
+ inject(c, "executionService", mock(AssessmentExecutionService.class));
|
|
|
+ inject(c, "userMapper", mock(UserMapper.class));
|
|
|
+
|
|
|
+ when(quotaSvc.getById(5L)).thenReturn(null);
|
|
|
+ Map<String, Object> body = new HashMap<>();
|
|
|
+ body.put("quotaId", 5L);
|
|
|
+ Result<?> r = c.detail(body);
|
|
|
+ assertEquals(500, r.getCode());
|
|
|
+ assertTrue(r.getMessage().contains("额度不存在"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== controller:AssessmentAppointmentController ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-P-36/SC-X-01 正常:创建预约 → 消耗额度 + 建预约 + 建执行记录")
|
|
|
+ void appointmentController_正常_创建() throws Exception {
|
|
|
+ AssessmentAppointmentService apptSvc = mock(AssessmentAppointmentService.class);
|
|
|
+ AssessmentQuotaService quotaSvc = mock(AssessmentQuotaService.class);
|
|
|
+ DanAssessmentExecutionService execSvc = mock(DanAssessmentExecutionService.class);
|
|
|
+ UserMapper userMapper = mock(UserMapper.class);
|
|
|
+ AssessmentAppointmentController c = new AssessmentAppointmentController();
|
|
|
+ inject(c, "appointmentService", apptSvc);
|
|
|
+ inject(c, "quotaService", quotaSvc);
|
|
|
+ inject(c, "danAssessmentExecutionService", execSvc);
|
|
|
+ inject(c, "userMapper", userMapper);
|
|
|
+
|
|
|
+ User user = new User();
|
|
|
+ user.setId(1L);
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(user);
|
|
|
+ when(quotaSvc.consumeQuota(5L, 10L)).thenReturn(2);
|
|
|
+ AssessmentQuota quota = quota(5L, "active", 3, 2, future());
|
|
|
+ quota.setProductOrderId(11L);
|
|
|
+ quota.setFamilyId(7L);
|
|
|
+ when(quotaSvc.getById(5L)).thenReturn(quota);
|
|
|
+
|
|
|
+ AssessmentAppointment appt = new AssessmentAppointment();
|
|
|
+ appt.setId(101L);
|
|
|
+ when(apptSvc.createAppointment(eq(1L), eq(10L), eq(7L), isNull(), isNull(),
|
|
|
+ eq("2026-11-10"), eq("10:00"), eq("首测")))
|
|
|
+ .thenReturn(appt);
|
|
|
+
|
|
|
+ DanAssessmentExecution exec = new DanAssessmentExecution();
|
|
|
+ exec.setId(201L);
|
|
|
+ when(execSvc.createExecution(11L, 10L, 7L, 5L, 7L, null, 101L)).thenReturn(exec);
|
|
|
+
|
|
|
+ Map<String, Object> body = new HashMap<>();
|
|
|
+ body.put("memberId", 10L);
|
|
|
+ body.put("guideId", 7L);
|
|
|
+ body.put("quotaId", 5L);
|
|
|
+ body.put("appointmentDate", "2026-11-10");
|
|
|
+ body.put("appointmentTime", "10:00");
|
|
|
+ body.put("notes", "首测");
|
|
|
+
|
|
|
+ Result<Map<String, Object>> r = c.createAppointment(1L, body, null);
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
+ assertEquals(101L, r.getData().get("appointmentId"));
|
|
|
+ assertEquals(201L, r.getData().get("executionId"));
|
|
|
+ verify(quotaSvc).consumeQuota(5L, 10L);
|
|
|
+ verify(execSvc).createExecution(11L, 10L, 7L, 5L, 7L, null, 101L);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-P-36 异常:memberId/quotaId 为空 → 业务提示")
|
|
|
+ void appointmentController_异常_缺参数() throws Exception {
|
|
|
+ AssessmentAppointmentController c = new AssessmentAppointmentController();
|
|
|
+ inject(c, "appointmentService", mock(AssessmentAppointmentService.class));
|
|
|
+ inject(c, "quotaService", mock(AssessmentQuotaService.class));
|
|
|
+ inject(c, "danAssessmentExecutionService", mock(DanAssessmentExecutionService.class));
|
|
|
+ inject(c, "userMapper", mock(UserMapper.class));
|
|
|
+
|
|
|
+ Result<?> r = c.createAppointment(1L, new HashMap<>(), null);
|
|
|
+ assertEquals(500, r.getCode());
|
|
|
+ assertTrue(r.getMessage().contains("memberId和quotaId不能为空"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-P-36 异常:assessorId/guideId 都为空 → 业务提示")
|
|
|
+ void appointmentController_异常_缺指派() throws Exception {
|
|
|
+ AssessmentAppointmentController c = new AssessmentAppointmentController();
|
|
|
+ inject(c, "appointmentService", mock(AssessmentAppointmentService.class));
|
|
|
+ inject(c, "quotaService", mock(AssessmentQuotaService.class));
|
|
|
+ inject(c, "danAssessmentExecutionService", mock(DanAssessmentExecutionService.class));
|
|
|
+ inject(c, "userMapper", mock(UserMapper.class));
|
|
|
+
|
|
|
+ Map<String, Object> body = new HashMap<>();
|
|
|
+ body.put("memberId", 10L);
|
|
|
+ body.put("quotaId", 5L);
|
|
|
+ Result<?> r = c.createAppointment(1L, body, null);
|
|
|
+ assertEquals(500, r.getCode());
|
|
|
+ assertTrue(r.getMessage().contains("assessorId或guideId必须提供一个"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-P-36 异常:额度不足 → 业务提示(不建预约)")
|
|
|
+ void appointmentController_异常_额度不足() throws Exception {
|
|
|
+ AssessmentAppointmentService apptSvc = mock(AssessmentAppointmentService.class);
|
|
|
+ AssessmentQuotaService quotaSvc = mock(AssessmentQuotaService.class);
|
|
|
+ UserMapper userMapper = mock(UserMapper.class);
|
|
|
+ AssessmentAppointmentController c = new AssessmentAppointmentController();
|
|
|
+ inject(c, "appointmentService", apptSvc);
|
|
|
+ inject(c, "quotaService", quotaSvc);
|
|
|
+ inject(c, "danAssessmentExecutionService", mock(DanAssessmentExecutionService.class));
|
|
|
+ inject(c, "userMapper", userMapper);
|
|
|
+
|
|
|
+ User user = new User();
|
|
|
+ user.setId(1L);
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(user);
|
|
|
+ when(quotaSvc.consumeQuota(5L, 10L)).thenReturn(0);
|
|
|
+
|
|
|
+ Map<String, Object> body = new HashMap<>();
|
|
|
+ body.put("memberId", 10L);
|
|
|
+ body.put("guideId", 7L);
|
|
|
+ body.put("quotaId", 5L);
|
|
|
+ Result<?> r = c.createAppointment(1L, body, null);
|
|
|
+ assertEquals(500, r.getCode());
|
|
|
+ assertTrue(r.getMessage().contains("额度不足或已过期"));
|
|
|
+ verify(apptSvc, never()).createAppointment(anyLong(), anyLong(), anyLong(), any(), any(), any(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-P-36 权限:用户不存在 → 业务提示")
|
|
|
+ void appointmentController_权限_用户不存在() throws Exception {
|
|
|
+ AssessmentAppointmentController c = new AssessmentAppointmentController();
|
|
|
+ inject(c, "appointmentService", mock(AssessmentAppointmentService.class));
|
|
|
+ inject(c, "quotaService", mock(AssessmentQuotaService.class));
|
|
|
+ inject(c, "danAssessmentExecutionService", mock(DanAssessmentExecutionService.class));
|
|
|
+ UserMapper userMapper = mock(UserMapper.class);
|
|
|
+ inject(c, "userMapper", userMapper);
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(null);
|
|
|
+
|
|
|
+ Map<String, Object> body = new HashMap<>();
|
|
|
+ body.put("memberId", 10L);
|
|
|
+ body.put("guideId", 7L);
|
|
|
+ body.put("quotaId", 5L);
|
|
|
+ Result<?> r = c.createAppointment(1L, body, null);
|
|
|
+ assertEquals(500, r.getCode());
|
|
|
+ assertTrue(r.getMessage().contains("用户不存在"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-T-09 正常:被指派测评师确认 → 成功")
|
|
|
+ void appointmentController_正常_确认() throws Exception {
|
|
|
+ AssessmentAppointmentService apptSvc = mock(AssessmentAppointmentService.class);
|
|
|
+ AssessmentAppointmentController c = new AssessmentAppointmentController();
|
|
|
+ inject(c, "appointmentService", apptSvc);
|
|
|
+ inject(c, "quotaService", mock(AssessmentQuotaService.class));
|
|
|
+ inject(c, "danAssessmentExecutionService", mock(DanAssessmentExecutionService.class));
|
|
|
+ inject(c, "userMapper", mock(UserMapper.class));
|
|
|
+
|
|
|
+ when(apptSvc.confirmAppointment(101L, 5L)).thenReturn(true);
|
|
|
+ Result<Boolean> r = c.confirmAppointment(5L, 101L);
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
+ assertEquals(true, r.getData());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-T-09 权限:非指派确认 → 业务提示「确认失败」")
|
|
|
+ void appointmentController_权限_非指派确认() throws Exception {
|
|
|
+ AssessmentAppointmentService apptSvc = mock(AssessmentAppointmentService.class);
|
|
|
+ AssessmentAppointmentController c = new AssessmentAppointmentController();
|
|
|
+ inject(c, "appointmentService", apptSvc);
|
|
|
+ inject(c, "quotaService", mock(AssessmentQuotaService.class));
|
|
|
+ inject(c, "danAssessmentExecutionService", mock(DanAssessmentExecutionService.class));
|
|
|
+ inject(c, "userMapper", mock(UserMapper.class));
|
|
|
+
|
|
|
+ when(apptSvc.confirmAppointment(101L, 9L)).thenReturn(false);
|
|
|
+ Result<Boolean> r = c.confirmAppointment(9L, 101L);
|
|
|
+ assertEquals(500, r.getCode());
|
|
|
+ assertTrue(r.getMessage().contains("确认失败"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-P-37 正常:我的预约列表")
|
|
|
+ void appointmentController_正常_我的列表() throws Exception {
|
|
|
+ AssessmentAppointmentService apptSvc = mock(AssessmentAppointmentService.class);
|
|
|
+ AssessmentAppointmentController c = new AssessmentAppointmentController();
|
|
|
+ inject(c, "appointmentService", apptSvc);
|
|
|
+ inject(c, "quotaService", mock(AssessmentQuotaService.class));
|
|
|
+ inject(c, "danAssessmentExecutionService", mock(DanAssessmentExecutionService.class));
|
|
|
+ inject(c, "userMapper", mock(UserMapper.class));
|
|
|
+
|
|
|
+ AssessmentAppointment a = new AssessmentAppointment();
|
|
|
+ a.setId(101L);
|
|
|
+ a.setStatus(1);
|
|
|
+ a.setAppointmentDate("2026-11-10");
|
|
|
+ when(apptSvc.getByUserId(1L)).thenReturn(Collections.singletonList(a));
|
|
|
+
|
|
|
+ Result<List<Map<String, Object>>> r = c.getMyAppointments(1L);
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
+ assertEquals(1, r.getData().size());
|
|
|
+ assertEquals("已确认", r.getData().get(0).get("statusText"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-T-13 权限:非规划师查 guide-list → 业务提示")
|
|
|
+ void appointmentController_权限_非规划师guideList() throws Exception {
|
|
|
+ AssessmentAppointmentController c = new AssessmentAppointmentController();
|
|
|
+ inject(c, "appointmentService", mock(AssessmentAppointmentService.class));
|
|
|
+ inject(c, "quotaService", mock(AssessmentQuotaService.class));
|
|
|
+ inject(c, "danAssessmentExecutionService", mock(DanAssessmentExecutionService.class));
|
|
|
+ inject(c, "userMapper", mock(UserMapper.class));
|
|
|
+
|
|
|
+ Result<?> r = c.getGuideAppointments(1L, "parent");
|
|
|
+ assertEquals(500, r.getCode());
|
|
|
+ assertTrue(r.getMessage().contains("只有成长规划师"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== service:AssessmentAppointmentService ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-T-09 正常:指派测评师确认 → status=1")
|
|
|
+ void appointmentService_正常_确认() throws Exception {
|
|
|
+ AssessmentAppointmentMapper mapper = mock(AssessmentAppointmentMapper.class);
|
|
|
+ AssessmentAppointmentService svc = new AssessmentAppointmentService();
|
|
|
+ inject(svc, "assessmentAppointmentMapper", mapper);
|
|
|
+ inject(svc, "energyService", mock(EnergyService.class));
|
|
|
+
|
|
|
+ AssessmentAppointment a = new AssessmentAppointment();
|
|
|
+ a.setId(101L);
|
|
|
+ a.setAssessorId(5L);
|
|
|
+ a.setStatus(0);
|
|
|
+ when(mapper.selectById(101L)).thenReturn(a);
|
|
|
+ when(mapper.updateById(any())).thenReturn(1);
|
|
|
+
|
|
|
+ boolean ok = svc.confirmAppointment(101L, 5L);
|
|
|
+ assertTrue(ok);
|
|
|
+ assertEquals(1, a.getStatus());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-T-09 权限:非指派确认 → false(状态不变)")
|
|
|
+ void appointmentService_权限_非指派确认() throws Exception {
|
|
|
+ AssessmentAppointmentMapper mapper = mock(AssessmentAppointmentMapper.class);
|
|
|
+ AssessmentAppointmentService svc = new AssessmentAppointmentService();
|
|
|
+ inject(svc, "assessmentAppointmentMapper", mapper);
|
|
|
+ inject(svc, "energyService", mock(EnergyService.class));
|
|
|
+
|
|
|
+ AssessmentAppointment a = new AssessmentAppointment();
|
|
|
+ a.setId(101L);
|
|
|
+ a.setAssessorId(5L);
|
|
|
+ a.setStatus(0);
|
|
|
+ when(mapper.selectById(101L)).thenReturn(a);
|
|
|
+
|
|
|
+ boolean ok = svc.confirmAppointment(101L, 9L);
|
|
|
+ assertEquals(false, ok);
|
|
|
+ assertEquals(0, a.getStatus(), "非指派不得改状态");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-T-09 异常:预约不存在 → false")
|
|
|
+ void appointmentService_异常_预约不存在() throws Exception {
|
|
|
+ AssessmentAppointmentMapper mapper = mock(AssessmentAppointmentMapper.class);
|
|
|
+ AssessmentAppointmentService svc = new AssessmentAppointmentService();
|
|
|
+ inject(svc, "assessmentAppointmentMapper", mapper);
|
|
|
+ inject(svc, "energyService", mock(EnergyService.class));
|
|
|
+
|
|
|
+ when(mapper.selectById(101L)).thenReturn(null);
|
|
|
+ assertEquals(false, svc.confirmAppointment(101L, 5L));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-T-10 正常:guide 完成 → status=2 且发放能量 30")
|
|
|
+ void appointmentService_正常_完成() throws Exception {
|
|
|
+ AssessmentAppointmentMapper mapper = mock(AssessmentAppointmentMapper.class);
|
|
|
+ AssessmentAppointmentService svc = new AssessmentAppointmentService();
|
|
|
+ inject(svc, "assessmentAppointmentMapper", mapper);
|
|
|
+ EnergyService energySvc = mock(EnergyService.class);
|
|
|
+ inject(svc, "energyService", energySvc);
|
|
|
+
|
|
|
+ AssessmentAppointment a = new AssessmentAppointment();
|
|
|
+ a.setId(101L);
|
|
|
+ a.setGuideId(7L);
|
|
|
+ a.setMemberId(10L);
|
|
|
+ a.setStatus(1);
|
|
|
+ when(mapper.selectById(101L)).thenReturn(a);
|
|
|
+ when(mapper.updateById(any())).thenReturn(1);
|
|
|
+
|
|
|
+ boolean ok = svc.completeAppointment(101L, 7L);
|
|
|
+ assertTrue(ok);
|
|
|
+ assertEquals(2, a.getStatus());
|
|
|
+ verify(energySvc).awardEnergy(eq(10L), eq("appointment"), eq(101L), eq(30), any(), isNull());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-T-10 权限:非 guide 完成 → false")
|
|
|
+ void appointmentService_权限_非guide完成() throws Exception {
|
|
|
+ AssessmentAppointmentMapper mapper = mock(AssessmentAppointmentMapper.class);
|
|
|
+ AssessmentAppointmentService svc = new AssessmentAppointmentService();
|
|
|
+ inject(svc, "assessmentAppointmentMapper", mapper);
|
|
|
+ inject(svc, "energyService", mock(EnergyService.class));
|
|
|
+
|
|
|
+ AssessmentAppointment a = new AssessmentAppointment();
|
|
|
+ a.setId(101L);
|
|
|
+ a.setGuideId(7L);
|
|
|
+ a.setStatus(1);
|
|
|
+ when(mapper.selectById(101L)).thenReturn(a);
|
|
|
+
|
|
|
+ assertEquals(false, svc.completeAppointment(101L, 9L));
|
|
|
+ assertEquals(1, a.getStatus(), "非指派不得置为完成");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-P-38 观察:取消预约无越权校验(仅置 status=3,观察钉住不修实现)")
|
|
|
+ void appointmentService_观察_取消无越权() throws Exception {
|
|
|
+ AssessmentAppointmentMapper mapper = mock(AssessmentAppointmentMapper.class);
|
|
|
+ AssessmentAppointmentService svc = new AssessmentAppointmentService();
|
|
|
+ inject(svc, "assessmentAppointmentMapper", mapper);
|
|
|
+ inject(svc, "energyService", mock(EnergyService.class));
|
|
|
+
|
|
|
+ AssessmentAppointment a = new AssessmentAppointment();
|
|
|
+ a.setId(101L);
|
|
|
+ a.setUserId(1L);
|
|
|
+ a.setStatus(1);
|
|
|
+ when(mapper.selectById(101L)).thenReturn(a);
|
|
|
+ when(mapper.updateById(any())).thenReturn(1);
|
|
|
+
|
|
|
+ // 契约缺口观察:cancelAppointment(id) 不接收 userId/role,任何调用方可取消
|
|
|
+ boolean ok = svc.cancelAppointment(101L);
|
|
|
+ assertTrue(ok);
|
|
|
+ assertEquals(3, a.getStatus());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== helpers ====================
|
|
|
+
|
|
|
+ private static AssessmentQuota quota(Long id, String status, int total, int remaining, Date validityEnd) {
|
|
|
+ AssessmentQuota q = new AssessmentQuota();
|
|
|
+ q.setId(id);
|
|
|
+ q.setQuotaNo("Q" + id);
|
|
|
+ q.setFamilyId(7L);
|
|
|
+ q.setMemberId(null);
|
|
|
+ q.setProductId(900L);
|
|
|
+ q.setProductName("DAN测评套餐");
|
|
|
+ q.setTotalSessions(total);
|
|
|
+ q.setRemainingSessions(remaining);
|
|
|
+ q.setValidityStart(new Date());
|
|
|
+ q.setValidityEnd(validityEnd);
|
|
|
+ q.setStatus(status);
|
|
|
+ return q;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Date future() {
|
|
|
+ return new Date(System.currentTimeMillis() + 30L * 24 * 3600 * 1000);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Date past() {
|
|
|
+ return new Date(System.currentTimeMillis() - 30L * 24 * 3600 * 1000);
|
|
|
+ }
|
|
|
+}
|