Browse Source

test(flow): 成长记录/能量系统/商品购买/签到奖励流程测试

Xiaogang Liao 4 months ago
parent
commit
06c28a47c1

+ 322 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/DailyCheckinStreakFlowTest.java

@@ -0,0 +1,322 @@
+package com.etotem.cfc.integration;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.DailyCheckinController;
+import com.etotem.cfc.controller.StreakController;
+import com.etotem.cfc.entity.DailyHealthCheckin;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.service.DailyCheckinService;
+import com.etotem.cfc.service.StreakService;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.mapper.ChildMapper;
+import org.junit.jupiter.api.*;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * ================================================================
+ * 场景:连续签到奖励全流程
+ * ================================================================
+ * 用户故事:作为孩子,我希望每天完成健康打卡,
+ *           保持连续签到记录以获得更多奖励。
+ *
+ * 流程步骤:
+ *   1. 孩子每天完成健康打卡(饮食、运动、睡眠、饮水、心情)
+ *   2. 系统记录打卡并更新连续签到天数
+ *   3. 达到特定天数阈值时发放奖励(积分或能量)
+ *   4. 第二天继续打卡以保持连续
+ *   5. 如果中断,系统重置连续天数为0
+ *
+ * 关键里程碑:
+ *   - [Milestone-1] 打卡记录创建成功
+ *   - [Milestone-2] 连续签到天数正确累计
+ *   - [Milestone-3] 达到奖励阈值,奖励发放
+ *   - [Milestone-4] 中断后重置连续天数
+ * ================================================================
+ */
+@SpringBootTest
+public class DailyCheckinStreakFlowTest {
+
+    @Resource
+    private DailyCheckinController checkinController;
+
+    @Resource
+    private StreakController streakController;
+
+    @MockBean
+    private DailyCheckinService dailyCheckinService;
+
+    @MockBean
+    private StreakService streakService;
+
+    @MockBean
+    private UserMapper userMapper;
+
+    @MockBean
+    private ChildMapper childMapper;
+
+    // ========== 测试数据 ==========
+
+    private static final Long USER_ID = 1001L;
+    private static final Long CHILD_ID = 2001L;
+    private static final Long FAMILY_ID = 3001L;
+    private static final Long CHECKIN_ID = 1L;
+
+    // ========== 场景1:每日健康打卡 ==========
+
+    /**
+     * 场景:孩子完成每日健康打卡
+     * 角色:孩子
+     * 触发条件:每天进入打卡页面填写健康数据
+     */
+    @Test
+    @DisplayName("[场景1] 孩子完成每日打卡 - 期望打卡成功")
+    void childDailyCheckin_Success() {
+        // === Given:孩子填写了打卡数据 ===
+        DailyHealthCheckin checkin = new DailyHealthCheckin();
+        checkin.setMemberId(CHILD_ID);
+        checkin.setDietScore(85);
+        checkin.setExerciseScore(90);
+        checkin.setSleepScore(80);
+        checkin.setWaterIntake(2000);
+        checkin.setMoodScore(88);
+
+        DailyHealthCheckin savedCheckin = new DailyHealthCheckin();
+        savedCheckin.setId(CHECKIN_ID);
+        savedCheckin.setMemberId(CHILD_ID);
+        savedCheckin.setDietScore(85);
+
+        when(dailyCheckinService.createCheckin(any(DailyHealthCheckin.class)))
+                .thenReturn(savedCheckin);
+
+        // === When:提交打卡 ===
+        Result<DailyHealthCheckin> result = checkinController.create(checkin);
+
+        // === Then:Milestone-1 - 打卡成功 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(CHECKIN_ID, result.getData().getId());
+        verify(dailyCheckinService).createCheckin(any(DailyHealthCheckin.class));
+    }
+
+    // ========== 场景2:查看打卡列表 ==========
+
+    /**
+     * 场景:查看历史打卡记录
+     * 角色:孩子或家长
+     * 触发条件:进入打卡历史页面
+     */
+    @Test
+    @DisplayName("[场景2] 查看打卡记录列表 - 期望返回历史记录")
+    void parentViewsCheckinList_Success() {
+        // === Given:有多条打卡记录 ===
+        Map<String, Object> listRequest = new HashMap<>();
+        listRequest.put("memberId", CHILD_ID);
+
+        List<DailyHealthCheckin> mockList = Arrays.asList(
+                createCheckin(1L, CHILD_ID, "2026-07-01", 85),
+                createCheckin(2L, CHILD_ID, "2026-06-30", 82),
+                createCheckin(3L, CHILD_ID, "2026-06-29", 88)
+        );
+
+        when(dailyCheckinService.getCheckinsByMember(CHILD_ID)).thenReturn(mockList);
+
+        // === When:请求打卡列表 ===
+        Result<List<DailyHealthCheckin>> result = checkinController.list(listRequest);
+
+        // === Then:返回历史记录 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(3, result.getData().size());
+        verify(dailyCheckinService).getCheckinsByMember(CHILD_ID);
+    }
+
+    // ========== 场景3:获取连续签到进度 ==========
+
+    /**
+     * 场景:孩子查看自己的连续签到进度
+     * 角色:孩子
+     * 触发条件:查看签到激励页面
+     */
+    @Test
+    @DisplayName("[场景3] 获取连续签到进度 - 期望返回当前连续天数和奖励状态")
+    void childGetsStreakProgress_Success() {
+        // === Given:孩子已连续打卡多天 ===
+        User user = new User();
+        user.setId(USER_ID);
+        user.setFamilyId(FAMILY_ID);
+
+        Child child = new Child();
+        child.setId(CHILD_ID);
+        child.setFamilyId(FAMILY_ID);
+
+        Map<String, Object> mockProgress = new HashMap<>();
+        mockProgress.put("childId", CHILD_ID);
+        mockProgress.put("currentStreak", 7);
+        mockProgress.put("longestStreak", 15);
+        mockProgress.put("todayChecked", true);
+        mockProgress.put("nextRewardDays", 3); // 再连续3天可获奖励
+        mockProgress.put("pendingReward", true); // 有待领取奖励
+
+        when(userMapper.selectById(USER_ID)).thenReturn(user);
+        when(childMapper.selectById(CHILD_ID)).thenReturn(child);
+        when(streakService.getStreakProgress(CHILD_ID)).thenReturn(mockProgress);
+
+        // === When:请求签到进度 ===
+        Result<Map<String, Object>> result = streakController.getStreakProgress(USER_ID, CHILD_ID);
+
+        // === Then:Milestone-2 - 返回连续天数等信息 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(7, result.getData().get("currentStreak"));
+        assertEquals(15, result.getData().get("longestStreak"));
+        assertTrue((Boolean) result.getData().get("todayChecked"));
+    }
+
+    // ========== 场景4:达到奖励阈值发放奖励 ==========
+
+    /**
+     * 场景:孩子连续签到达到7天阈值,系统发放奖励
+     * 角色:系统(自动触发)
+     * 触发条件:连续签到天数达到特定阈值
+     */
+    @Test
+    @DisplayName("[场景4] 连续7天打卡达成 - 期望奖励发放")
+    void childReaches7DayStreak_RewardGranted() {
+        // === Given:连续打卡达到7天 ===
+        when(streakService.checkAndGrantReward(eq(CHILD_ID), eq(7)))
+                .thenReturn(true);
+
+        // === When:系统检查并发放奖励 ===
+        Result<Boolean> result = streakController.checkAndGrantReward(CHILD_ID, 7);
+
+        // === Then:Milestone-3 - 奖励发放成功 ===
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+        verify(streakService).checkAndGrantReward(eq(CHILD_ID), eq(7));
+    }
+
+    /**
+     * 场景:孩子连续签到达到30天,获得大奖励
+     * 角色:系统
+     * 触发条件:连续签到达到30天
+     */
+    @Test
+    @DisplayName("[场景4b] 连续30天打卡达成 - 期望获得大额奖励")
+    void childReaches30DayStreak_GrandRewardGranted() {
+        // === Given:连续打卡达到30天 ===
+        when(streakService.checkAndGrantReward(eq(CHILD_ID), eq(30)))
+                .thenReturn(true);
+
+        // === When:系统发放大奖励 ===
+        Result<Boolean> result = streakController.checkAndGrantReward(CHILD_ID, 30);
+
+        // === Then:奖励发放 ===
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+        verify(streakService).checkAndGrantReward(eq(CHILD_ID), eq(30));
+    }
+
+    // ========== 场景5:打卡中断重置 ==========
+
+    /**
+     * 场景:孩子忘记打卡,连续签到中断
+     * 角色:系统(定时任务触发)
+     * 触发条件:凌晨检查发现昨天未打卡
+     */
+    @Test
+    @DisplayName("[场景5] 打卡中断 - 期望连续天数重置")
+    void childMissesCheckin_StreakReset() {
+        // === Given:昨天未打卡 ===
+        when(streakService.resetStreakIfNeeded()).thenReturn(true);
+
+        // === When:系统重置中断的签到 ===
+        Result<Boolean> result = streakController.resetStreak();
+
+        // === Then:Milestone-4 - 连续天数重置 ===
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+        verify(streakService).resetStreakIfNeeded();
+    }
+
+    // ========== 场景6:更新打卡记录 ==========
+
+    /**
+     * 场景:孩子补充或修改今天的打卡数据
+     * 角色:孩子
+     * 触发条件:发现打卡数据填错需要修改
+     */
+    @Test
+    @DisplayName("[场景6] 更新打卡记录 - 期望更新成功")
+    void childUpdatesCheckin_Success() {
+        // === Given:需要修改打卡数据 ===
+        Map<String, Object> updateRequest = new HashMap<>();
+        updateRequest.put("id", CHECKIN_ID);
+        updateRequest.put("dietScore", 90); // 原本85,修改为90
+        updateRequest.put("moodScore", 95);
+
+        DailyHealthCheckin updatedCheckin = new DailyHealthCheckin();
+        updatedCheckin.setId(CHECKIN_ID);
+        updatedCheckin.setDietScore(90);
+        updatedCheckin.setMoodScore(95);
+
+        when(dailyCheckinService.updateCheckin(eq(CHECKIN_ID), any(DailyHealthCheckin.class)))
+                .thenReturn(updatedCheckin);
+
+        // === When:更新打卡记录 ===
+        Result<DailyHealthCheckin> result = checkinController.update(updateRequest);
+
+        // === Then:更新成功 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(90, result.getData().getDietScore());
+        assertEquals(95, result.getData().getMoodScore());
+    }
+
+    // ========== 场景7:删除打卡记录 ==========
+
+    /**
+     * 场景:孩子删除一条错误的打卡记录
+     * 角色:孩子
+     * 触发条件:发现打卡记录有误需要删除
+     */
+    @Test
+    @DisplayName("[场景7] 删除打卡记录 - 期望删除成功")
+    void childDeletesCheckin_Success() {
+        // === Given:需要删除记录 ===
+        Map<String, Object> deleteRequest = new HashMap<>();
+        deleteRequest.put("id", CHECKIN_ID);
+
+        doNothing().when(dailyCheckinService).deleteCheckin(CHECKIN_ID);
+
+        // === When:删除打卡记录 ===
+        Result<String> result = checkinController.delete(deleteRequest);
+
+        // === Then:删除成功 ===
+        assertEquals(200, result.getCode());
+        assertEquals("删除成功", result.getData());
+        verify(dailyCheckinService).deleteCheckin(CHECKIN_ID);
+    }
+
+    // ========== Helper Methods ==========
+
+    private DailyHealthCheckin createCheckin(Long id, Long memberId, String checkinDate, Integer score) {
+        DailyHealthCheckin checkin = new DailyHealthCheckin();
+        checkin.setId(id);
+        checkin.setMemberId(memberId);
+        checkin.setCheckinDate(checkinDate);
+        checkin.setDietScore(score);
+        checkin.setExerciseScore(score);
+        checkin.setSleepScore(score);
+        checkin.setMoodScore(score);
+        return checkin;
+    }
+}

+ 302 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/EnergySystemFlowTest.java

@@ -0,0 +1,302 @@
+package com.etotem.cfc.integration;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.EnergyController;
+import com.etotem.cfc.entity.EnergyLog;
+import com.etotem.cfc.dto.EnergySandboxDTO;
+import com.etotem.cfc.service.EnergyService;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.entity.User;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.junit.jupiter.api.*;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * ================================================================
+ * 场景:能量系统收支全流程
+ * ================================================================
+ * 用户故事:作为家长,我希望了解和管理家庭成员的五维能量收支情况,
+ *           通过完成任务获取能量,用能量兑换奖励。
+ *
+ * 流程步骤:
+ *   1. 查看家庭能量沙盘,了解各维度能量分布
+ *   2. 孩子完成任务,获得能量奖励(收入)
+ *   3. 使用能量兑换心愿或参与活动(支出)
+ *   4. 系统根据收支计算新的维度比例
+ *   5. 综合健康指数更新
+ *
+ * 关键里程碑:
+ *   - [Milestone-1] 获取能量沙盘,了解当前能量状态
+ *   - [Milestone-2] 任务完成,能量发放成功
+ *   - [Milestone-3] 能量兑换,积分扣减
+ *   - [Milestone-4] 维度比例重算
+ *   - [Milestone-5] 健康指数更新
+ * ================================================================
+ */
+@SpringBootTest
+public class EnergySystemFlowTest {
+
+    @Resource
+    private EnergyController energyController;
+
+    @MockBean
+    private EnergyService energyService;
+
+    @MockBean
+    private UserMapper userMapper;
+
+    // ========== 测试数据 ==========
+
+    private static final Long PARENT_USER_ID = 1001L;
+    private static final Long CHILD_ID = 2001L;
+    private static final Long FAMILY_ID = 3001L;
+
+    // ========== 场景1:获取家庭能量沙盘 ==========
+
+    /**
+     * 场景:家长查看家庭的五维能量分布
+     * 角色:家长
+     * 触发条件:进入能量沙盘页面
+     */
+    @Test
+    @DisplayName("[场景1] 获取家庭能量沙盘 - 期望返回五维能量数据")
+    void parentGetsFamilyEnergySandbox_Success() {
+        // === Given:存在家庭能量数据 ===
+        User parent = new User();
+        parent.setId(PARENT_USER_ID);
+        parent.setFamilyId(FAMILY_ID);
+
+        EnergySandboxDTO mockSandbox = createMockEnergySandbox();
+
+        when(userMapper.selectById(PARENT_USER_ID)).thenReturn(parent);
+        when(energyService.calculateFamilyEnergy(FAMILY_ID)).thenReturn(mockSandbox);
+
+        // === When:请求能量沙盘数据 ===
+        Result<EnergySandboxDTO> result = energyController.getFamilyEnergySandbox(PARENT_USER_ID);
+
+        // === Then:Milestone-1 - 返回完整能量数据 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertNotNull(result.getData().getBody());    // 身
+        assertNotNull(result.getData().getMind());    // 心
+        assertNotNull(result.getData().getWisdom());  // 智
+        assertNotNull(result.getData().getAction()); // 行
+        assertNotNull(result.getData().getWealth());  // 富
+        verify(energyService).calculateFamilyEnergy(FAMILY_ID);
+    }
+
+    /**
+     * 场景:用户未加入任何家庭
+     * 角色:用户
+     * 触发条件:用户尝试查看能量但未加入家庭
+     */
+    @Test
+    @DisplayName("[场景1b] 用户未加入家庭 - 期望返回错误提示")
+    void userWithoutFamily_Fails() {
+        // === Given:用户未加入家庭 ===
+        User user = new User();
+        user.setId(PARENT_USER_ID);
+        user.setFamilyId(null);
+
+        when(userMapper.selectById(PARENT_USER_ID)).thenReturn(user);
+
+        // === When:请求能量沙盘 ===
+        Result<EnergySandboxDTO> result = energyController.getFamilyEnergySandbox(PARENT_USER_ID);
+
+        // === Then:返回错误 ===
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("未加入家庭"));
+    }
+
+    // ========== 场景2:任务完成能量发放 ==========
+
+    /**
+     * 场景:孩子完成任务后,系统自动发放能量奖励
+     * 角色:系统(自动触发)
+     * 触发条件:任务状态变更为"已完成"
+     */
+    @Test
+    @DisplayName("[场景2] 任务完成能量发放 - 期望能量增加且流水记录")
+    void taskCompletionGrantsEnergy_Success() {
+        // === Given:任务完成,需要发放能量 ===
+        Long taskId = 1L;
+        Long energyAmount = 100L;
+        String dimensionCode = "action"; // 行
+
+        EnergyLog mockLog = createEnergyLog(CHILD_ID, "income", energyAmount, dimensionCode, "任务奖励");
+
+        when(energyService.grantEnergy(eq(CHILD_ID), eq(taskId), eq(energyAmount), eq(dimensionCode)))
+                .thenReturn(mockLog);
+
+        // === When:触发能量发放 ===
+        Result<EnergyLog> result = energyController.grantEnergy(CHILD_ID, taskId, energyAmount, dimensionCode);
+
+        // === Then:Milestone-2 - 能量发放成功 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(energyAmount, result.getData().getEnergyAmount());
+        verify(energyService).grantEnergy(eq(CHILD_ID), eq(taskId), eq(energyAmount), eq(dimensionCode));
+    }
+
+    // ========== 场景3:能量兑换心愿 ==========
+
+    /**
+     * 场景:孩子使用积累的能量兑换心愿
+     * 角色:孩子
+     * 触发条件:兑换心愿时选择能量支付
+     */
+    @Test
+    @DisplayName("[场景3] 能量兑换心愿 - 期望能量扣减")
+    void childExchangesWishWithEnergy_Success() {
+        // === Given:孩子有足够的能量 ===
+        Long wishId = 1L;
+        Long energyDeduct = 500L;
+
+        when(energyService.deductForWish(eq(CHILD_ID), eq(wishId), eq(energyDeduct)))
+                .thenReturn(true);
+
+        // === When:使用能量兑换心愿 ===
+        Result<Boolean> result = energyController.deductForWish(CHILD_ID, wishId, energyDeduct);
+
+        // === Then:Milestone-3 - 能量扣减成功 ===
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+        verify(energyService).deductForWish(eq(CHILD_ID), eq(wishId), eq(energyDeduct));
+    }
+
+    /**
+     * 场景:能量不足时兑换心愿
+     * 角色:孩子
+     * 触发条件:当前能量不足以支付
+     */
+    @Test
+    @DisplayName("[场景3b] 能量不足时兑换 - 期望返回错误")
+    void childExchangesWithInsufficientEnergy_Fails() {
+        // === Given:能量不足 ===
+        Long wishId = 1L;
+        Long energyDeduct = 5000L; // 超过可用能量
+
+        when(energyService.deductForWish(eq(CHILD_ID), eq(wishId), eq(energyDeduct)))
+                .thenThrow(new RuntimeException("能量不足,当前可用:300,需要:5000"));
+
+        // === When:尝试兑换 ===
+        Result<Boolean> result = energyController.deductForWish(CHILD_ID, wishId, energyDeduct);
+
+        // === Then:返回错误 ===
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("能量不足"));
+    }
+
+    // ========== 场景4:查询能量流水 ==========
+
+    /**
+     * 场景:查看能量收支明细
+     * 角色:家长或孩子
+     * 触发条件:进入能量明细页面
+     */
+    @Test
+    @DisplayName("[场景4] 查询能量流水 - 期望返回分页记录")
+    void parentQueriesEnergyLogs_Success() {
+        // === Given:有多条能量流水记录 ===
+        Map<String, Object> request = new HashMap<>();
+        request.put("childId", CHILD_ID);
+        request.put("dimensionCode", "action");
+        request.put("page", 1);
+        request.put("size", 10);
+
+        Page<EnergyLog> mockPage = new Page<>(1, 10);
+        mockPage.setRecords(Arrays.asList(
+                createEnergyLog(CHILD_ID, "income", 100L, "action", "完成任务奖励"),
+                createEnergyLog(CHILD_ID, "expense", 50L, "action", "兑换心愿"),
+                createEnergyLog(CHILD_ID, "income", 80L, "body", "运动打卡")
+        ));
+
+        when(energyService.getLogs(eq(CHILD_ID), eq("action"), eq(1), eq(10)))
+                .thenReturn(mockPage);
+
+        // === When:查询能量流水 ===
+        Result<Page<EnergyLog>> result = energyController.getEnergyLogs(request);
+
+        // === Then:返回分页数据 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(3, result.getData().getRecords().size());
+    }
+
+    // ========== 场景5:获取五维能量概览 ==========
+
+    /**
+     * 场景:查看孩子的五维能量概览
+     * 角色:家长
+     * 触发条件:查看孩子的能量总览
+     */
+    @Test
+    @DisplayName("[场景5] 获取五维能量概览 - 期望返回五维汇总数据")
+    void parentGetsEnergyOverview_Success() {
+        // === Given:请求孩子的能量概览 ===
+        Map<String, Object> request = new HashMap<>();
+        request.put("childId", CHILD_ID);
+
+        Map<String, Object> mockOverview = new HashMap<>();
+        mockOverview.put("childId", CHILD_ID);
+        mockOverview.put("totalEnergy", 12500L);
+        mockOverview.put("bodyEnergy", 2500L);
+        mockOverview.put("mindEnergy", 2200L);
+        mockOverview.put("wisdomEnergy", 2800L);
+        mockOverview.put("actionEnergy", 2500L);
+        mockOverview.put("wealthEnergy", 2500L);
+        mockOverview.put("healthIndex", 87.5);
+        mockOverview.put("trend", "up");
+
+        when(energyService.getOverview(CHILD_ID)).thenReturn(mockOverview);
+
+        // === When:请求能量概览 ===
+        Result<Map<String, Object>> result = energyController.getEnergyOverview(request);
+
+        // === Then:Milestone-5 - 返回概览数据包含健康指数 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(12500L, result.getData().get("totalEnergy"));
+        assertEquals(87.5, result.getData().get("healthIndex"));
+        assertNotNull(result.getData().get("bodyEnergy"));
+        assertNotNull(result.getData().get("mindEnergy"));
+        assertNotNull(result.getData().get("wisdomEnergy"));
+        assertNotNull(result.getData().get("actionEnergy"));
+        assertNotNull(result.getData().get("wealthEnergy"));
+    }
+
+    // ========== Helper Methods ==========
+
+    private EnergySandboxDTO createMockEnergySandbox() {
+        EnergySandboxDTO dto = new EnergySandboxDTO();
+        dto.setFamilyId(FAMILY_ID);
+        dto.setTotalEnergy(25000L);
+        dto.setHealthIndex(85.5);
+        dto.setBody(2500L);
+        dto.setMind(2500L);
+        dto.setWisdom(2500L);
+        dto.setAction(2500L);
+        dto.setWealth(2500L);
+        return dto;
+    }
+
+    private EnergyLog createEnergyLog(Long childId, String type, Long amount, String dimension, String reason) {
+        EnergyLog log = new EnergyLog();
+        log.setId(1L);
+        log.setChildId(childId);
+        log.setType(type);
+        log.setEnergyAmount(amount);
+        log.setDimensionCode(dimension);
+        log.setReason(reason);
+        log.setCreatedAt(new Date());
+        return log;
+    }
+}

+ 274 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/GrowthRecordSyncFlowTest.java

@@ -0,0 +1,274 @@
+package com.etotem.cfc.integration;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.growth.GrowthRecordController;
+import com.etotem.cfc.entity.GrowthRecord;
+import com.etotem.cfc.entity.Family;
+import com.etotem.cfc.dto.ParsedReport;
+import com.etotem.cfc.service.GrowthRecordService;
+import com.etotem.cfc.service.AssessmentOrderService;
+import com.etotem.cfc.service.ReportParseService;
+import com.etotem.cfc.service.StorageService;
+import com.etotem.cfc.mapper.FamilyMapper;
+import org.junit.jupiter.api.*;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * ================================================================
+ * 场景:成长记录同步全流程
+ * ================================================================
+ * 用户故事:作为家长,我希望记录孩子的成长轨迹,
+ *           上传健康报告并与测评结果关联,获得全面的成长分析。
+ *
+ * 流程步骤:
+ *   1. 家长创建孩子的成长档案(基本信息、学校、年级等)
+ *   2. 家长上传孩子的健康体检报告
+ *   3. 系统解析报告,提取身高、体重、视力等数据
+ *   4. 成长记录与认知测评结果关联
+ *   5. 系统更新孩子的五维能量数据
+ *
+ * 关键里程碑:
+ *   - [Milestone-1] 成长档案创建成功
+ *   - [Milestone-2] 健康报告上传并解析成功
+ *   - [Milestone-3] 成长记录与测评报告关联
+ *   - [Milestone-4] 五维能量数据更新
+ * ================================================================
+ */
+@SpringBootTest
+public class GrowthRecordSyncFlowTest {
+
+    @Resource
+    private GrowthRecordController growthRecordController;
+
+    @MockBean
+    private GrowthRecordService growthRecordService;
+
+    @MockBean
+    private AssessmentOrderService assessmentOrderService;
+
+    @MockBean
+    private ReportParseService reportParseService;
+
+    @MockBean
+    private StorageService storageService;
+
+    @MockBean
+    private FamilyMapper familyMapper;
+
+    // ========== 测试数据 ==========
+
+    private static final Long PARENT_USER_ID = 1001L;
+    private static final Long CHILD_ID = 2001L;
+    private static final Long FAMILY_ID = 3001L;
+    private static final Long RECORD_ID = 1L;
+
+    // ========== 场景1:创建成长档案 ==========
+
+    /**
+     * 场景:家长为孩子创建一个成长档案
+     * 角色:家长
+     * 触发条件:家长首次使用成长记录功能
+     */
+    @Test
+    @DisplayName("[场景1] 创建孩子成长档案 - 期望档案创建成功")
+    void parentCreatesGrowthRecord_Success() {
+        // === Given:家长填写了孩子的基本信息 ===
+        GrowthRecord record = new GrowthRecord();
+        record.setChildId(CHILD_ID);
+        record.setSchool("北京市第一小学");
+        record.setGrade(3);
+        record.setAcademicPerformance("优秀");
+        record.setSchoolProvince("北京市");
+        record.setSchoolCity("北京市");
+        record.setSchoolDistrict("海淀区");
+
+        GrowthRecord savedRecord = new GrowthRecord();
+        savedRecord.setId(RECORD_ID);
+        savedRecord.setChildId(CHILD_ID);
+        savedRecord.setSchool("北京市第一小学");
+        savedRecord.setGrade(3);
+
+        when(growthRecordService.createRecord(any(GrowthRecord.class))).thenReturn(savedRecord);
+
+        // === When:家长提交创建成长档案 ===
+        Result<GrowthRecord> result = growthRecordController.createRecord(record, PARENT_USER_ID);
+
+        // === Then:Milestone-1 - 档案创建成功 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(RECORD_ID, result.getData().getId());
+        verify(growthRecordService).createRecord(any(GrowthRecord.class));
+    }
+
+    /**
+     * 场景:家长创建档案时未选择孩子
+     * 角色:家长
+     * 触发条件:表单中孩子ID为空
+     */
+    @Test
+    @DisplayName("[场景1b] 创建档案时未选择孩子 - 期望返回错误")
+    void parentCreatesRecordWithoutChild_Fails() {
+        // === Given:未选择孩子 ===
+        GrowthRecord record = new GrowthRecord();
+        record.setSchool("北京市第一小学");
+        record.setGrade(3);
+        // childId 为 null
+
+        // === When:尝试创建档案 ===
+        Result<GrowthRecord> result = growthRecordController.createRecord(record, PARENT_USER_ID);
+
+        // === Then:返回错误提示 ===
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("请选择孩子"));
+    }
+
+    // ========== 场景2:创建档案并购买测评 ==========
+
+    /**
+     * 场景:家长创建成长档案的同时购买认知测评服务
+     * 角色:家长
+     * 触发条件:勾选"同时购买测评"选项
+     */
+    @Test
+    @DisplayName("[场景2] 创建档案并购买测评套餐 - 期望档案和订单同时创建")
+    void parentCreatesRecordWithOrder_Success() {
+        // === Given:家长填写档案信息并选择同时购买测评 ===
+        Map<String, Object> request = new HashMap<>();
+        request.put("childId", CHILD_ID);
+        request.put("familyId", FAMILY_ID);
+        request.put("packageId", 5L);
+        request.put("school", "北京市第一小学");
+        request.put("grade", 3);
+        request.put("alsoPurchaseAssessment", true);
+
+        Family family = new Family();
+        family.setId(FAMILY_ID);
+        family.setTeacherId(4001L);
+
+        GrowthRecord savedRecord = new GrowthRecord();
+        savedRecord.setId(RECORD_ID);
+        savedRecord.setChildId(CHILD_ID);
+
+        when(familyMapper.selectById(FAMILY_ID)).thenReturn(family);
+        when(growthRecordService.createRecord(any(GrowthRecord.class))).thenReturn(savedRecord);
+
+        // === When:提交创建档案和购买测评 ===
+        Result<Map<String, Object>> result = growthRecordController.createRecordWithOrder(request, PARENT_USER_ID);
+
+        // === Then:档案创建成功,订单也创建 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertNotNull(result.getData().get("record"));
+        assertNotNull(result.getData().get("order"));
+        verify(growthRecordService).createRecord(any(GrowthRecord.class));
+    }
+
+    // ========== 场景3:上传健康报告并解析 ==========
+
+    /**
+     * 场景:家长上传孩子的健康体检报告PDF,系统自动解析
+     * 角色:家长
+     * 触发条件:上传体检报告文件
+     */
+    @Test
+    @DisplayName("[场景3] 上传并解析健康报告 - 期望解析出健康数据")
+    void parentUploadsHealthReport_Success() {
+        // === Given:上传了健康报告文件 ===
+        Long reportId = 1L;
+        String fileUrl = "https://storage.example.com/reports/health_2026_06.pdf";
+
+        ParsedReport parsedReport = new ParsedReport();
+        parsedReport.setHeight(135.5);
+        parsedReport.setWeight(32.0);
+        parsedReport.setVisionLeft(5.0);
+        parsedReport.setVisionRight(5.1);
+        parsedReport.setHeightPercentile(65);
+        parsedReport.setWeightPercentile(58);
+
+        when(reportParseService.parsePdf(eq(fileUrl))).thenReturn(parsedReport);
+
+        // === When:系统解析报告 ===
+        Result<ParsedReport> result = growthRecordController.uploadAndParseReport(
+                null, CHILD_ID, fileUrl, PARENT_USER_ID);
+
+        // === Then:Milestone-2 - 报告解析成功,返回提取的数据 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(135.5, result.getData().getHeight());
+        assertEquals(32.0, result.getData().getWeight());
+        verify(reportParseService).parsePdf(eq(fileUrl));
+    }
+
+    // ========== 场景4:关联成长记录与测评结果 ==========
+
+    /**
+     * 场景:成长记录与认知测评结果进行关联
+     * 角色:系统(自动关联)
+     * 触发条件:测评报告生成后
+     */
+    @Test
+    @DisplayName("[场景4] 成长记录关联测评报告 - 期望关联成功")
+    void growthRecordAssociatesWithAssessment_Success() {
+        // === Given:存在成长记录和测评结果 ===
+        GrowthRecord record = new GrowthRecord();
+        record.setId(RECORD_ID);
+        record.setChildId(CHILD_ID);
+
+        when(growthRecordService.associateWithAssessment(eq(RECORD_ID), anyLong())).thenReturn(true);
+
+        // === When:关联操作执行 ===
+        Result<Boolean> result = growthRecordController.associateWithAssessment(RECORD_ID, 1L);
+
+        // === Then:Milestone-3 - 关联成功 ===
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+        verify(growthRecordService).associateWithAssessment(eq(RECORD_ID), anyLong());
+    }
+
+    // ========== 场景5:查询孩子的成长记录列表 ==========
+
+    /**
+     * 场景:家长查看孩子的历史成长记录
+     * 角色:家长
+     * 触发条件:进入成长记录历史页面
+     */
+    @Test
+    @DisplayName("[场景5] 查询成长记录历史 - 期望返回记录列表")
+    void parentQueriesGrowthHistory_Success() {
+        // === Given:有多条成长记录 ===
+        List<GrowthRecord> mockRecords = Arrays.asList(
+                createGrowthRecord(1L, CHILD_ID, "2026-06-01", "本学期表现优异"),
+                createGrowthRecord(2L, CHILD_ID, "2026-03-01", "体测达标"),
+                createGrowthRecord(3L, CHILD_ID, "2025-12-01", "学习进步明显")
+        );
+
+        when(growthRecordService.getRecordsByChild(CHILD_ID)).thenReturn(mockRecords);
+
+        // === When:家长请求历史记录 ===
+        Result<List<GrowthRecord>> result = growthRecordController.getRecordsByChild(CHILD_ID);
+
+        // === Then:返回历史记录列表 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(3, result.getData().size());
+        verify(growthRecordService).getRecordsByChild(CHILD_ID);
+    }
+
+    // ========== Helper Methods ==========
+
+    private GrowthRecord createGrowthRecord(Long id, Long childId, String recordDate, String note) {
+        GrowthRecord record = new GrowthRecord();
+        record.setId(id);
+        record.setChildId(childId);
+        record.setNote(note);
+        return record;
+    }
+}

+ 301 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/ProductPurchaseFlowTest.java

@@ -0,0 +1,301 @@
+package com.etotem.cfc.integration;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.product.ProductOrderController;
+import com.etotem.cfc.dto.CreateProductOrderDTO;
+import com.etotem.cfc.dto.ProductOrderDTO;
+import com.etotem.cfc.service.ProductOrderService;
+import com.etotem.cfc.service.PaymentService;
+import org.junit.jupiter.api.*;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * ================================================================
+ * 场景:商品购买支付全流程
+ * ================================================================
+ * 用户故事:作为家长,我希望在商城购买孩子的用品,
+ *           完成支付后等待商品配送到家。
+ *
+ * 流程步骤:
+ *   1. 家长浏览商品列表,选择商品
+ *   2. 家长创建商品订单
+ *   3. 家长选择支付方式并完成支付
+ *   4. 服务商发货,填写物流信息
+ *   5. 家长确认收货,订单完成
+ *   6. 如有问题,家长申请退款
+ *
+ * 关键里程碑:
+ *   - [Milestone-1] 订单创建成功,获得订单号
+ *   - [Milestone-2] 支付成功,订单状态变更为"已支付"
+ *   - [Milestone-3] 服务商发货,订单状态变更为"已发货"
+ *   - [Milestone-4] 家长确认收货,订单完成
+ *   - [Milestone-5] 退款申请提交成功
+ * ================================================================
+ */
+@SpringBootTest
+public class ProductPurchaseFlowTest {
+
+    @Resource
+    private ProductOrderController orderController;
+
+    @MockBean
+    private ProductOrderService orderService;
+
+    @MockBean
+    private PaymentService paymentService;
+
+    // ========== 测试数据 ==========
+
+    private static final Long USER_ID = 1001L;
+    private static final String ORDER_NO = "PRD20260628001";
+    private static final String TRACKING_NO = "SF1234567890";
+    private static final String EXPRESS_COMPANY = "顺丰速运";
+
+    // ========== 场景1:创建商品订单 ==========
+
+    /**
+     * 场景:家长选购商品后创建订单
+     * 角色:家长
+     * 触发条件:点击"立即购买"或"加入购物车结算"
+     */
+    @Test
+    @DisplayName("[场景1] 创建商品订单 - 期望订单创建成功")
+    void parentCreatesOrder_Success() {
+        // === Given:家长选择了商品 ===
+        CreateProductOrderDTO dto = new CreateProductOrderDTO();
+        dto.setProductId(1L);
+        dto.setQuantity(2);
+        dto.setAddressId(10L);
+
+        ProductOrderDTO mockOrder = createProductOrder(ORDER_NO, "pending");
+        when(orderService.create(any(CreateProductOrderDTO.class), eq(USER_ID)))
+                .thenReturn(mockOrder);
+
+        // === When:提交订单创建 ===
+        Result<ProductOrderDTO> result = orderController.create(dto, USER_ID);
+
+        // === Then:Milestone-1 - 订单创建成功 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(ORDER_NO, result.getData().getOrderNo());
+        assertEquals("pending", result.getData().getStatus());
+        verify(orderService).create(any(CreateProductOrderDTO.class), eq(USER_ID));
+    }
+
+    // ========== 场景2:支付订单 ==========
+
+    /**
+     * 场景:家长选择微信支付完成订单支付
+     * 角色:家长
+     * 触发条件:点击"去支付"并选择支付方式
+     */
+    @Test
+    @DisplayName("[场景2] 支付商品订单 - 期望支付成功")
+    void parentPaysOrder_Success() {
+        // === Given:存在待支付订单 ===
+        Map<String, Object> payRequest = new HashMap<>();
+        payRequest.put("orderNo", ORDER_NO);
+
+        Map<String, Object> mockPayResult = new HashMap<>();
+        mockPayResult.put("orderNo", ORDER_NO);
+        mockPayResult.put("payTime", new Date());
+        mockPayResult.put("status", "paid");
+
+        when(orderService.pay(eq(ORDER_NO), eq(USER_ID))).thenReturn(Result.success(mockPayResult));
+
+        // === When:家长完成支付 ===
+        Result<Map<String, Object>> result = orderController.pay(payRequest, USER_ID);
+
+        // === Then:Milestone-2 - 支付成功 ===
+        assertEquals(200, result.getCode());
+        assertEquals("paid", result.getData().get("status"));
+        verify(orderService).pay(eq(ORDER_NO), eq(USER_ID));
+    }
+
+    // ========== 场景3:服务商发货 ==========
+
+    /**
+     * 场景:服务商收到订单后发货,填写物流信息
+     * 角色:服务商/供应商
+     * 触发条件:商家点击"发货"并填写物流单号
+     */
+    @Test
+    @DisplayName("[场景3] 服务商发货 - 期望订单状态更新为已发货")
+    void vendorShipsOrder_Success() {
+        // === Given:商家需要发货 ===
+        Map<String, Object> shipRequest = new HashMap<>();
+        shipRequest.put("orderNo", ORDER_NO);
+        shipRequest.put("trackingNumber", TRACKING_NO);
+        shipRequest.put("expressCompany", EXPRESS_COMPANY);
+
+        when(orderService.ship(eq(ORDER_NO), eq(TRACKING_NO), eq(EXPRESS_COMPANY)))
+                .thenReturn(Result.success("发货成功"));
+
+        // === When:服务商发货 ===
+        Result<String> result = orderController.ship(shipRequest);
+
+        // === Then:Milestone-3 - 发货成功 ===
+        assertEquals(200, result.getCode());
+        assertEquals("发货成功", result.getData());
+        verify(orderService).ship(eq(ORDER_NO), eq(TRACKING_NO), eq(EXPRESS_COMPANY));
+    }
+
+    // ========== 场景4:家长确认收货 ==========
+
+    /**
+     * 场景:家长收到商品后确认收货
+     * 角色:家长
+     * 触发条件:确认收到商品无误
+     */
+    @Test
+    @DisplayName("[场景4] 家长确认收货 - 期望订单完成")
+    void parentConfirmsReceive_Success() {
+        // === Given:家长已收到商品 ===
+        Map<String, Object> confirmRequest = new HashMap<>();
+        confirmRequest.put("orderNo", ORDER_NO);
+
+        when(orderService.confirmReceive(eq(ORDER_NO), eq(USER_ID)))
+                .thenReturn(Result.success("确认收货成功"));
+
+        // === When:家长点击"确认收货" ===
+        Result<String> result = orderController.confirm(confirmRequest, USER_ID);
+
+        // === Then:Milestone-4 - 订单完成 ===
+        assertEquals(200, result.getCode());
+        assertEquals("确认收货成功", result.getData());
+        verify(orderService).confirmReceive(eq(ORDER_NO), eq(USER_ID));
+    }
+
+    // ========== 场景5:申请退款 ==========
+
+    /**
+     * 场景:家长发现问题,申请退款
+     * 角色:家长
+     * 触发条件:点击"申请退款"并填写原因
+     */
+    @Test
+    @DisplayName("[场景5] 申请退款 - 期望退款申请提交成功")
+    void parentAppliesRefund_Success() {
+        // === Given:需要申请退款 ===
+        Map<String, Object> refundRequest = new HashMap<>();
+        refundRequest.put("orderNo", ORDER_NO);
+        refundRequest.put("reason", "商品与描述不符");
+
+        when(orderService.applyRefund(eq(ORDER_NO), eq(USER_ID), eq("商品与描述不符")))
+                .thenReturn(Result.success("退款申请已提交"));
+
+        // === When:提交退款申请 ===
+        Result<String> result = orderController.applyRefund(refundRequest, USER_ID);
+
+        // === Then:Milestone-5 - 退款申请成功 ===
+        assertEquals(200, result.getCode());
+        assertEquals("退款申请已提交", result.getData());
+        verify(orderService).applyRefund(eq(ORDER_NO), eq(USER_ID), eq("商品与描述不符"));
+    }
+
+    // ========== 场景6:取消未支付订单 ==========
+
+    /**
+     * 场景:家长改变主意,取消未支付的订单
+     * 角色:家长
+     * 触发条件:点击"取消订单"
+     */
+    @Test
+    @DisplayName("[场景6] 取消未支付订单 - 期望订单关闭")
+    void parentCancelsOrder_Success() {
+        // === Given:订单未支付 ===
+        Map<String, Object> cancelRequest = new HashMap<>();
+        cancelRequest.put("orderNo", ORDER_NO);
+
+        when(orderService.cancel(eq(ORDER_NO), eq(USER_ID)))
+                .thenReturn(Result.success("订单已取消"));
+
+        // === When:取消订单 ===
+        Result<String> result = orderController.cancel(cancelRequest, USER_ID);
+
+        // === Then:订单关闭 ===
+        assertEquals(200, result.getCode());
+        assertEquals("订单已取消", result.getData());
+    }
+
+    // ========== 场景7:查看我的订单列表 ==========
+
+    /**
+     * 场景:家长查看自己的订单列表
+     * 角色:家长
+     * 触发条件:进入"我的订单"页面
+     */
+    @Test
+    @DisplayName("[场景7] 查看我的订单列表 - 期望返回订单列表")
+    void parentViewsMyOrders_Success() {
+        // === Given:用户有多个订单 ===
+        Map<String, Object> listRequest = new HashMap<>();
+        listRequest.put("status", "all");
+
+        List<ProductOrderDTO> mockOrders = Arrays.asList(
+                createProductOrder("ORD001", "completed"),
+                createProductOrder("ORD002", "paid"),
+                createProductOrder("ORD003", "pending")
+        );
+
+        when(orderService.myOrders(eq(USER_ID), eq("all"))).thenReturn(Result.success(mockOrders));
+
+        // === When:请求订单列表 ===
+        Result<List<ProductOrderDTO>> result = orderController.myOrders(listRequest, USER_ID);
+
+        // === Then:返回订单列表 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(3, result.getData().size());
+    }
+
+    // ========== 场景8:查看订单详情 ==========
+
+    /**
+     * 场景:家长查看具体订单的详细信息
+     * 角色:家长
+     * 触发条件:点击某个订单查看详情
+     */
+    @Test
+    @DisplayName("[场景8] 查看订单详情 - 期望返回完整订单信息")
+    void parentViewsOrderDetail_Success() {
+        // === Given:订单存在 ===
+        Map<String, Object> detailRequest = new HashMap<>();
+        detailRequest.put("orderNo", ORDER_NO);
+
+        ProductOrderDTO mockDetail = createProductOrder(ORDER_NO, "paid");
+        mockDetail.setTrackingNumber(TRACKING_NO);
+        mockDetail.setExpressCompany(EXPRESS_COMPANY);
+
+        when(orderService.detail(eq(ORDER_NO), eq(USER_ID))).thenReturn(Result.success(mockDetail));
+
+        // === When:请求订单详情 ===
+        Result<ProductOrderDTO> result = orderController.detail(detailRequest, USER_ID);
+
+        // === Then:返回完整订单信息 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(ORDER_NO, result.getData().getOrderNo());
+        assertEquals(TRACKING_NO, result.getData().getTrackingNumber());
+    }
+
+    // ========== Helper Methods ==========
+
+    private ProductOrderDTO createProductOrder(String orderNo, String status) {
+        ProductOrderDTO order = new ProductOrderDTO();
+        order.setOrderNo(orderNo);
+        order.setStatus(status);
+        order.setTotalAmount(29900L);
+        order.setProductName("儿童科学实验套装");
+        order.setQuantity(1);
+        return order;
+    }
+}