Переглянути джерело

feat(backend): 新增今日任务聚合服务 DailyOverviewService + 只读接口 POST /api/daily-task/overview

- DailyOverviewVO 聚合打卡/任务/阅读活动/测评报告四类今日状态
- DailyOverviewService 纯只读,复用现有 healthCheckin/financeCheckin/emotionCheckin/growthTask/task/articleReading/assessment/danReport 查询
- DailyTaskController memberId 回退链:请求体 memberId → currentMemberId
- 单元测试 DailyOverviewServiceTest + DailyTaskControllerTest(@SpringBootTest + @MockBean)
asus 2 місяців тому
батько
коміт
e6325b2849

+ 40 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/DailyTaskController.java

@@ -0,0 +1,40 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.DailyOverviewVO;
+import com.etotem.cfc.service.DailyOverviewService;
+import io.swagger.v3.oas.annotations.Operation;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.Map;
+
+/**
+ * 今日任务聚合接口(只读)
+ */
+@RestController
+@RequestMapping("/api/daily-task")
+public class DailyTaskController {
+
+    @Resource
+    private DailyOverviewService dailyOverviewService;
+
+    @Operation(summary = "今日任务总览(打卡/任务/阅读活动/测评报告四类)")
+    @PostMapping("/overview")
+    public Result<DailyOverviewVO> overview(
+            @RequestBody(required = false) Map<String, Object> params,
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId,
+            @RequestAttribute(value = "familyId", required = false) Long familyId) {
+        Long memberId = (params != null && params.get("memberId") != null)
+                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        if (memberId == null) {
+            return Result.error("memberId不能为空");
+        }
+        return Result.success(dailyOverviewService.aggregate(userId, memberId, familyId));
+    }
+}

+ 51 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/DailyOverviewVO.java

@@ -0,0 +1,51 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 今日任务聚合视图(只读)
+ */
+@Data
+public class DailyOverviewVO {
+
+    private Long memberId;
+    private String date; // yyyy-MM-dd
+    private List<CheckinItem> checkins;
+    private TaskSummary tasks;
+    private ActivitySummary activities;
+    private AssessmentSummary assessments;
+    private Boolean hasMore = false; // 二期「查看全部」扩展位
+
+    @Data
+    public static class CheckinItem {
+        private String key;      // health / wealth / mind
+        private String label;    // 健康打卡 / 财商打卡 / 情绪打卡
+        private Boolean done;
+        private String route;    // 由后端下发,前端直接 navigateTo
+    }
+
+    @Data
+    public static class TaskSummary {
+        private Integer pendingAccept; // 家庭任务待接受数
+        private Integer todoToday;     // 今日待完成数(成长任务 DAILY)
+        private Integer doneToday;     // 今日已完成数
+        private String route;          // /pages/tasks/tasks
+    }
+
+    @Data
+    public static class ActivitySummary {
+        private Integer articleReadToday; // 今日已读文章数
+        private Integer activityAvailable; // v1 恒为 0(预留)
+        private String articleRoute;       // /pages/article-center/index
+        private String activityRoute;      // /pages/activity/index
+    }
+
+    @Data
+    public static class AssessmentSummary {
+        private Integer ongoing;      // 进行中测评数
+        private Integer reportPending; // 待上传/待确认报告数
+        private String route;         // /pages/assessment/results
+    }
+}

+ 173 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DailyOverviewService.java

@@ -0,0 +1,173 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.dto.DailyOverviewVO;
+import com.etotem.cfc.dto.DanReportUploadVO;
+import com.etotem.cfc.entity.ArticleReadingRecord;
+import com.etotem.cfc.entity.AssessmentRecord;
+import com.etotem.cfc.entity.Task;
+import com.etotem.cfc.mapper.ArticleReadingRecordMapper;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * 今日任务聚合服务(只读,零新业务规则,全部复用现有查询)
+ */
+@Service
+public class DailyOverviewService {
+
+    @Resource
+    private HealthCheckinService healthCheckinService;
+    @Resource
+    private FinanceCheckinService financeCheckinService;
+    @Resource
+    private EmotionCheckinService emotionCheckinService;
+    @Resource
+    private GrowthTaskService growthTaskService;
+    @Resource
+    private TaskService taskService;
+    @Resource
+    private ArticleReadingRecordMapper articleReadingRecordMapper;
+    @Resource
+    private AssessmentService assessmentService;
+    @Resource
+    private DanReportUploadService danReportUploadService;
+
+    private static final String TODAY_FMT = "yyyy-MM-dd";
+
+    public DailyOverviewVO aggregate(Long userId, Long memberId, Long familyId) {
+        String today = new SimpleDateFormat(TODAY_FMT).format(new Date());
+        DailyOverviewVO vo = new DailyOverviewVO();
+        vo.setMemberId(memberId);
+        vo.setDate(today);
+        vo.setCheckins(buildCheckins(userId, memberId, today));
+        vo.setTasks(buildTasks(userId, memberId));
+        vo.setActivities(buildActivities(memberId));
+        vo.setAssessments(buildAssessments(memberId, familyId));
+        vo.setHasMore(false);
+        return vo;
+    }
+
+    // ---------- 打卡类(最多 3 项:健康/财商/情绪) ----------
+
+    private List<DailyOverviewVO.CheckinItem> buildCheckins(Long userId, Long memberId, String today) {
+        List<DailyOverviewVO.CheckinItem> list = new ArrayList<>();
+        list.add(buildCheckin("health", "健康打卡", "/pages/health/daily-checkin",
+                hasTodayRecord(healthCheckinService.getCheckins(userId, memberId, null), today,
+                        o -> ((com.etotem.cfc.entity.HealthCheckin) o).getCheckinDate())));
+        list.add(buildCheckin("wealth", "财商打卡", "/pages/wealth-sub/checkin",
+                hasTodayRecord(financeCheckinService.getCheckins(userId, memberId, null), today,
+                        o -> ((com.etotem.cfc.entity.FinanceCheckin) o).getCheckinDate())));
+        list.add(buildCheckin("mind", "情绪打卡", "/pages/mind-detail/emotion-checkin",
+                hasTodayRecord(emotionCheckinService.getCheckinList(memberId, 1, 1, userId, true), today,
+                        o -> ((com.etotem.cfc.dto.EmotionCheckinVO) o).getCheckinDate())));
+        return list;
+    }
+
+    private DailyOverviewVO.CheckinItem buildCheckin(String key, String label, String route, boolean done) {
+        DailyOverviewVO.CheckinItem item = new DailyOverviewVO.CheckinItem();
+        item.setKey(key);
+        item.setLabel(label);
+        item.setRoute(route);
+        item.setDone(done);
+        return item;
+    }
+
+    /** 今日过滤:dateGetter 返回 java.util.Date 或 String,统一格式化为 yyyy-MM-dd 后与 today 比较 */
+    private boolean hasTodayRecord(List<?> records, String today, java.util.function.Function<Object, Object> dateGetter) {
+        if (records == null || records.isEmpty()) return false;
+        SimpleDateFormat fmt = new SimpleDateFormat(TODAY_FMT);
+        for (Object rec : records) {
+            Object d = dateGetter.apply(rec);
+            if (d == null) continue;
+            String ds;
+            if (d instanceof java.util.Date) {
+                ds = fmt.format((java.util.Date) d);
+            } else {
+                ds = String.valueOf(d);
+            }
+            if (ds != null && ds.startsWith(today)) return true;
+        }
+        return false;
+    }
+
+    // ---------- 任务类 ----------
+
+    private DailyOverviewVO.TaskSummary buildTasks(Long userId, Long memberId) {
+        DailyOverviewVO.TaskSummary s = new DailyOverviewVO.TaskSummary();
+        s.setPendingAccept(0);
+
+        List<Task> pending = taskService.getMyPendingTasks(memberId);
+        s.setPendingAccept(pending == null ? 0 : pending.size());
+
+        Map<String, String> params = new HashMap<>();
+        params.put("type", "DAILY");
+        List<Map<String, Object>> dailyList = growthTaskService.getTaskList(userId, params);
+        int todo = 0;
+        int done = 0;
+        if (dailyList != null) {
+            for (Map<String, Object> t : dailyList) {
+                Object c = t.get("completed");
+                boolean isDone = c != null && ("1".equals(String.valueOf(c)) || Boolean.TRUE.equals(c));
+                if (isDone) done++; else todo++;
+            }
+        }
+        s.setTodoToday(todo);
+        s.setDoneToday(done);
+        s.setRoute("/pages/tasks/tasks");
+        return s;
+    }
+
+    // ---------- 阅读活动类 ----------
+
+    private DailyOverviewVO.ActivitySummary buildActivities(Long memberId) {
+        DailyOverviewVO.ActivitySummary s = new DailyOverviewVO.ActivitySummary();
+        Long count = articleReadingRecordMapper.selectCount(
+                new LambdaQueryWrapper<ArticleReadingRecord>()
+                        .eq(ArticleReadingRecord::getChildId, memberId)
+                        .apply("DATE(read_at) = CURDATE()"));
+        s.setArticleReadToday(count == null ? 0 : count.intValue());
+        s.setActivityAvailable(0); // v1 无现成可参与活动数查询,预留
+        s.setArticleRoute("/pages/article-center/index");
+        s.setActivityRoute("/pages/activity/index");
+        return s;
+    }
+
+    // ---------- 测评报告类 ----------
+
+    private DailyOverviewVO.AssessmentSummary buildAssessments(Long memberId, Long familyId) {
+        DailyOverviewVO.AssessmentSummary s = new DailyOverviewVO.AssessmentSummary();
+
+        int ongoing = 0;
+        if (familyId != null) {
+            List<AssessmentRecord> records = assessmentService.getFamilyRecords(familyId);
+            if (records != null) {
+                for (AssessmentRecord r : records) {
+                    if ("pending".equals(r.getStatus()) && memberId.equals(r.getChildId())) {
+                        ongoing++;
+                    }
+                }
+            }
+        }
+        s.setOngoing(ongoing);
+
+        int reportPending = 0;
+        try {
+            com.etotem.cfc.common.Result<List<DanReportUploadVO>> uploadRes =
+                    danReportUploadService.listByMember(memberId, null);
+            if (uploadRes != null && uploadRes.getCode() == 200 && uploadRes.getData() != null) {
+                for (DanReportUploadVO vo : uploadRes.getData()) {
+                    if ("pending".equals(vo.getDraftStatus())) reportPending++;
+                }
+            }
+        } catch (Exception e) {
+            reportPending = 0; // DAN 报告子模块异常不阻断聚合
+        }
+        s.setReportPending(reportPending);
+        s.setRoute("/pages/assessment/results");
+        return s;
+    }
+}

+ 54 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/DailyTaskControllerTest.java

@@ -0,0 +1,54 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.DailyOverviewVO;
+import com.etotem.cfc.service.DailyOverviewService;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+@SpringBootTest
+public class DailyTaskControllerTest {
+
+    @Resource
+    private DailyTaskController controller;
+
+    @MockBean
+    private DailyOverviewService dailyOverviewService;
+
+    @Test
+    public void overview_成功_返回聚合视图() {
+        DailyOverviewVO vo = new DailyOverviewVO();
+        vo.setMemberId(10L);
+        vo.setCheckins(Collections.emptyList());
+        when(dailyOverviewService.aggregate(eq(1L), eq(10L), eq(100L))).thenReturn(vo);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("memberId", 10L);
+
+        Result<DailyOverviewVO> result = controller.overview(params, 1L, 10L, 100L);
+
+        assertEquals(200, result.getCode());
+        assertEquals(10L, result.getData().getMemberId().longValue());
+    }
+
+    @Test
+    public void overview_memberId为null_返回错误() {
+        Map<String, Object> params = new HashMap<>();
+
+        Result<DailyOverviewVO> result = controller.overview(params, 1L, null, 100L);
+
+        assertEquals(500, result.getCode());
+        assertEquals("memberId不能为空", result.getMessage());
+    }
+}

+ 90 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/DailyOverviewServiceTest.java

@@ -0,0 +1,90 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.DailyOverviewVO;
+import com.etotem.cfc.dto.DanReportUploadVO;
+import com.etotem.cfc.entity.AssessmentRecord;
+import com.etotem.cfc.entity.Task;
+import com.etotem.cfc.mapper.ArticleReadingRecordMapper;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import org.junit.jupiter.api.Test;
+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.when;
+
+@SpringBootTest
+public class DailyOverviewServiceTest {
+
+    @Resource
+    private DailyOverviewService service;
+
+    @MockBean
+    private HealthCheckinService healthCheckinService;
+    @MockBean
+    private FinanceCheckinService financeCheckinService;
+    @MockBean
+    private EmotionCheckinService emotionCheckinService;
+    @MockBean
+    private GrowthTaskService growthTaskService;
+    @MockBean
+    private TaskService taskService;
+    @MockBean
+    private ArticleReadingRecordMapper articleReadingRecordMapper;
+    @MockBean
+    private AssessmentService assessmentService;
+    @MockBean
+    private DanReportUploadService danReportUploadService;
+
+    @Test
+    public void aggregate_四类汇总_全部正确() {
+        // 打卡:全部空列表 → 均未完成
+        when(healthCheckinService.getCheckins(any(), any(), any())).thenReturn(Collections.emptyList());
+        when(financeCheckinService.getCheckins(any(), any(), any())).thenReturn(Collections.emptyList());
+        when(emotionCheckinService.getCheckinList(any(), anyInt(), anyInt(), any(), anyBoolean()))
+                .thenReturn(Collections.emptyList());
+
+        // 任务:待接受 1 条;成长任务 已完成1 + 待完成1
+        when(taskService.getMyPendingTasks(10L))
+                .thenReturn(Collections.singletonList(new Task()));
+        Map<String, Object> doneTask = new HashMap<>();
+        doneTask.put("completed", 1);
+        Map<String, Object> todoTask = new HashMap<>();
+        todoTask.put("completed", 0);
+        when(growthTaskService.getTaskList(eq(1L), anyMap()))
+                .thenReturn(Arrays.asList(doneTask, todoTask));
+
+        // 阅读:今日已读 2 条
+        when(articleReadingRecordMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(2L);
+
+        // 测评:进行中 1(pending 且 childId 匹配);待上传报告 0
+        AssessmentRecord rec = new AssessmentRecord();
+        rec.setStatus("pending");
+        rec.setChildId(10L);
+        when(assessmentService.getFamilyRecords(100L))
+                .thenReturn(Collections.singletonList(rec));
+        when(danReportUploadService.listByMember(10L, null))
+                .thenReturn(Result.success(Collections.<DanReportUploadVO>emptyList()));
+
+        DailyOverviewVO vo = service.aggregate(1L, 10L, 100L);
+
+        assertEquals(10L, vo.getMemberId().longValue());
+        assertEquals(3, vo.getCheckins().size());
+        assertFalse(vo.getCheckins().get(0).getDone());
+
+        assertEquals(Integer.valueOf(1), vo.getTasks().getPendingAccept());
+        assertEquals(Integer.valueOf(1), vo.getTasks().getTodoToday());
+        assertEquals(Integer.valueOf(1), vo.getTasks().getDoneToday());
+
+        assertEquals(Integer.valueOf(2), vo.getActivities().getArticleReadToday());
+
+        assertEquals(Integer.valueOf(1), vo.getAssessments().getOngoing());
+        assertEquals(Integer.valueOf(0), vo.getAssessments().getReportPending());
+    }
+}