|
@@ -0,0 +1,142 @@
|
|
|
|
|
+package com.etotem.cfc.unit;
|
|
|
|
|
+
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
+import com.etotem.cfc.entity.FamilyMember;
|
|
|
|
|
+import com.etotem.cfc.entity.PointsLog;
|
|
|
|
|
+import com.etotem.cfc.entity.Task;
|
|
|
|
|
+import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.PointsLogMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.TaskExecutionMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.TaskMapper;
|
|
|
|
|
+import com.etotem.cfc.service.EnergyService;
|
|
|
|
|
+import com.etotem.cfc.service.FamilyPlatformPointsService;
|
|
|
|
|
+import com.etotem.cfc.service.PlatformPointsService;
|
|
|
|
|
+import com.etotem.cfc.service.StreakService;
|
|
|
|
|
+import com.etotem.cfc.service.TaskService;
|
|
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
|
|
+import org.mockito.ArgumentCaptor;
|
|
|
|
|
+
|
|
|
|
|
+import java.lang.reflect.Field;
|
|
|
|
|
+import java.util.Collections;
|
|
|
|
|
+
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
|
|
+import static org.mockito.Mockito.atLeastOnce;
|
|
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 统一任务进度回写单元测试
|
|
|
|
|
+ * 覆盖:
|
|
|
|
|
+ * 1. 显式 familyMemberId 精确更新(同一 userId 绑定多个家庭成员时不误更新他人任务)
|
|
|
|
|
+ * 2. 向后兼容:未传 familyMemberId 时按 userId 反查
|
|
|
|
|
+ * 3. 无 deadline 的每日打卡任务完成发放基础分,不误判"迟到"扣分
|
|
|
|
|
+ */
|
|
|
|
|
+class TaskProgressUpdateTest {
|
|
|
|
|
+
|
|
|
|
|
+ private TaskService service;
|
|
|
|
|
+ private TaskMapper taskMapper;
|
|
|
|
|
+ private FamilyMemberMapper familyMemberMapper;
|
|
|
|
|
+ private TaskExecutionMapper taskExecutionMapper;
|
|
|
|
|
+ private PointsLogMapper pointsLogMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @BeforeEach
|
|
|
|
|
+ void setUp() throws Exception {
|
|
|
|
|
+ service = new TaskService();
|
|
|
|
|
+ taskMapper = mock(TaskMapper.class);
|
|
|
|
|
+ familyMemberMapper = mock(FamilyMemberMapper.class);
|
|
|
|
|
+ taskExecutionMapper = mock(TaskExecutionMapper.class);
|
|
|
|
|
+ pointsLogMapper = mock(PointsLogMapper.class);
|
|
|
|
|
+ inject("taskMapper", taskMapper);
|
|
|
|
|
+ inject("familyMemberMapper", familyMemberMapper);
|
|
|
|
|
+ inject("taskExecutionMapper", taskExecutionMapper);
|
|
|
|
|
+ inject("pointsLogMapper", pointsLogMapper);
|
|
|
|
|
+ inject("streakService", mock(StreakService.class));
|
|
|
|
|
+ inject("energyService", mock(EnergyService.class));
|
|
|
|
|
+ inject("platformPointsService", mock(PlatformPointsService.class));
|
|
|
|
|
+ inject("familyPlatformPointsService", mock(FamilyPlatformPointsService.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void inject(String field, Object value) throws Exception {
|
|
|
|
|
+ Field f = TaskService.class.getDeclaredField(field);
|
|
|
|
|
+ f.setAccessible(true);
|
|
|
|
|
+ f.set(service, value);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private FamilyMember member(long id, long userId, boolean penaltyEnabled) {
|
|
|
|
|
+ FamilyMember m = new FamilyMember();
|
|
|
|
|
+ m.setId(id);
|
|
|
|
|
+ m.setUserId(userId);
|
|
|
|
|
+ m.setFamilyId(1L);
|
|
|
|
|
+ m.setTotalPoints(100);
|
|
|
|
|
+ m.setStreakDays(0);
|
|
|
|
|
+ m.setPenaltyEnabled(penaltyEnabled ? 1 : 0);
|
|
|
|
|
+ return m;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Task dietTask(long id, long familyMemberId) {
|
|
|
|
|
+ Task t = new Task();
|
|
|
|
|
+ t.setId(id);
|
|
|
|
|
+ t.setTitle("[DIET]饮食打卡");
|
|
|
|
|
+ t.setStatus("pending");
|
|
|
|
|
+ t.setPoints(5);
|
|
|
|
|
+ t.setTargetValue(1);
|
|
|
|
|
+ t.setIsDailyProgress(1);
|
|
|
|
|
+ t.setFamilyMemberId(familyMemberId);
|
|
|
|
|
+ t.setFamilyId(1L);
|
|
|
|
|
+ t.setDeadline(null);
|
|
|
|
|
+ t.setBonusRule(null);
|
|
|
|
|
+ return t;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void updateProgress_withExplicitFamilyMemberId_doesNotFallbackToUserIdLookup() {
|
|
|
|
|
+ // 同一 userId 绑定多个家庭成员时,必须按显式 familyMemberId 精确更新
|
|
|
|
|
+ when(familyMemberMapper.selectById(12L)).thenReturn(member(12L, 12L, false));
|
|
|
|
|
+ // 第一次为 DIET 任务,第二次为嵌套的 DAILY_TASK 查询(返回 null)
|
|
|
|
|
+ when(taskMapper.selectOne(any(LambdaQueryWrapper.class)))
|
|
|
|
|
+ .thenReturn(dietTask(1758L, 12L), null);
|
|
|
|
|
+ when(taskExecutionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(null);
|
|
|
|
|
+ when(taskMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Collections.emptyList());
|
|
|
|
|
+
|
|
|
|
|
+ service.updateProgress(12L, 12L, "DIET", 1);
|
|
|
|
|
+
|
|
|
|
|
+ // 关键断言:显式指定成员时不做 user_id 反查(避免命中错误成员的任务)
|
|
|
|
|
+ verify(familyMemberMapper, atLeastOnce()).selectById(12L);
|
|
|
|
|
+ verify(familyMemberMapper, never()).selectOne(any());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void updateProgress_withoutFamilyMemberId_fallsBackToUserIdLookup() {
|
|
|
|
|
+ when(familyMemberMapper.selectOne(any(LambdaQueryWrapper.class)))
|
|
|
|
|
+ .thenReturn(member(12L, 12L, false));
|
|
|
|
|
+ when(taskMapper.selectOne(any(LambdaQueryWrapper.class)))
|
|
|
|
|
+ .thenReturn(dietTask(1758L, 12L), null);
|
|
|
|
|
+ when(taskExecutionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(null);
|
|
|
|
|
+ when(taskMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Collections.emptyList());
|
|
|
|
|
+
|
|
|
|
|
+ service.updateProgress(12L, "DIET", 1);
|
|
|
|
|
+
|
|
|
|
|
+ verify(familyMemberMapper).selectOne(any(LambdaQueryWrapper.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void updateProgress_noDeadlineTask_awardsBasePointsInsteadOfPenalty() {
|
|
|
|
|
+ // 每日打卡任务无 deadline,即使开启扣分开关也不应被误判"迟到"扣分
|
|
|
|
|
+ when(familyMemberMapper.selectById(12L)).thenReturn(member(12L, 12L, true));
|
|
|
|
|
+ when(pointsLogMapper.selectSum(any(LambdaQueryWrapper.class))).thenReturn(0);
|
|
|
|
|
+ when(taskMapper.selectOne(any(LambdaQueryWrapper.class)))
|
|
|
|
|
+ .thenReturn(dietTask(1758L, 12L), null);
|
|
|
|
|
+ when(taskExecutionMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(null);
|
|
|
|
|
+ when(taskMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Collections.emptyList());
|
|
|
|
|
+
|
|
|
|
|
+ service.updateProgress(12L, 12L, "DIET", 1);
|
|
|
|
|
+
|
|
|
|
|
+ ArgumentCaptor<PointsLog> captor = ArgumentCaptor.forClass(PointsLog.class);
|
|
|
|
|
+ verify(pointsLogMapper).insert(captor.capture());
|
|
|
|
|
+ assertEquals(5, captor.getValue().getAmount());
|
|
|
|
|
+ }
|
|
|
|
|
+}
|