Просмотр исходного кода

docs(superpowers): 今日任务首页卡片+只读聚合接口实施计划

- 后端 DailyOverviewService 聚合打卡/任务/阅读活动/测评报告四类
- Task1 聚合服务 + Task2 接口 POST /api/daily-task/overview + Task3 前端卡片 + Task4 首页集成
- 引用签名已对照实际代码自检(checkinDate/hasTodayRecord 等)
asus 2 месяцев назад
Родитель
Сommit
29976afa63
1 измененных файлов с 912 добавлено и 0 удалено
  1. 912 0
      docs/superpowers/plans/2026-08-15-daily-task-overview.md

+ 912 - 0
docs/superpowers/plans/2026-08-15-daily-task-overview.md

@@ -0,0 +1,912 @@
+# 「今日任务」首页卡片 + 只读聚合接口 实施计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 首页新增「今日任务」卡片,聚合打卡/任务/阅读活动/测评报告四类今日状态,点击跳转对应功能页,返回自动刷新。
+
+**Architecture:** 后端新增 1 个只读聚合接口 `POST /api/daily-task/overview`(`DailyTaskController` + `DailyOverviewService` + `DailyOverviewVO`),复用现有 service/mapper 查询逻辑,零表结构改动;前端新增 `components/daily-task-card.vue` 组件(自带 onShow 拉取),挂载到首页 index-home。
+
+**Tech Stack:** Spring Boot 2.7.18 + MyBatis-Plus + Java 8;uni-app Vue 2(Options API)微信小程序。
+
+## Global Constraints
+
+- 接口统一 `@PostMapping`,禁止 `@GetMapping/@PutMapping/@DeleteMapping`(AGENTS.md)
+- 响应统一 `Result<T>`(code=200/message/data),`Result.noFamily` code=5001(Result.java)
+- DI 用 `@Resource`,字段名与类型默认 Bean Name 一致(AGENTS.md)
+- 认证:JWT Bearer Token;当前用户 = `@RequestAttribute("userId")`,当前家庭成员 = `@RequestAttribute(value="currentMemberId", required=false)`,家庭 = `@RequestAttribute(value="familyId", required=false)`;**禁止使用 `@RequestAttribute("memberId")`(代码库无此 attribute)**(JwtInterceptor.java:131-165)
+- memberId 回退链:请求体 `memberId` → `currentMemberId`(JwtInterceptor 注入)→ 成长任务层用 `userId`(GrowthTaskService.getTaskList 以 userId 为参数)
+- 聚合接口只读:**不写库、不改任何表结构**(spec §4.2)
+- 小程序限制:禁止可选链 `?.`(用 `&&`/逐字段判断)、禁止 CSS Grid(用 flexbox)、禁止 `:key` 表达式(用方法调用 `:key="getXxxKey(item)"`)、禁止 `new Date(string)`(用 `utils/format.js` 的 `parseDate()`)、数组 props 必须 `default: function() { return [] }`、:`class` 绑定禁用方法调用(cfc-frontend/AGENTS.md)
+- 前端打包禁止 npm build 命令,只做 `node --check` 语法校验,打包走 HBuilderX(cfc-frontend/AGENTS.md)
+- 后端验证:`cd cfc-backend && mvn clean compile`;测试:`mvn test`(无 H2/Testcontainers,靠 @MockBean 隔离 DB)
+
+---
+
+### Task 1: 后端聚合核心 — DailyOverviewVO + DailyOverviewService
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/DailyOverviewVO.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/DailyOverviewService.java`
+- Test: `cfc-backend/src/test/java/com/etotem/cfc/service/DailyOverviewServiceTest.java`
+
+**Interfaces:**
+- Consumes: 现有 service 方法(本文件列出签名,任务内逐一使用)
+- Produces: `DailyOverviewService.aggregate(Long userId, Long memberId, Long familyId)` → `DailyOverviewVO`(Task 2 的 Controller 调用)
+- Produces: `DailyOverviewVO` 结构(checkins/tasks/activities/assessments/hasMore),Task 3 前端按此渲染
+
+**前提校验步骤(开始编码前执行):**
+- [ ] 步骤 0: 确认实体字段名(若与下方代码不同则替换字段名,方法签名不变):
+  - `Resource: HealthCheckin.getCheckinDate()`(java.util.Date,列 checkin_date,MicroActionService.java:187-193 已用 `DATE(checkin_date)`)
+  - `Resource: FinanceCheckin.getCheckinDate()`(java.util.Date,同列名约定)
+  - `Resource: EmotionCheckinVO.getCheckinDate()`(java.util.Date,dto/EmotionCheckinVO.java:18,与 health/finance 统一用 checkinDate)
+  - `Resource: ArticleReadingRecord.getChildId()`(列 child_id)与 `getReadAt()`(列 read_at,ArticleService.java:321 已用 `DATE(read_at)=CURDATE()`)
+  - `Resource: AssessmentRecord.getStatus()/getChildId()`、`DanReportUploadVO.getDraftStatus()`(dto/DanReportUploadVO.java:26,常量 `DanReportUpload.DRAFT_PENDING="pending"`,entity/DanReportUpload.java:71)
+  - 若某字段名不同,改下方代码中对应字段引用,其余不动
+
+- [ ] **Step 1: 写失败测试**
+
+创建 `DailyOverviewServiceTest.java`:
+
+```java
+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(2);
+
+        // 测评:进行中 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());
+    }
+}
+```
+
+- [ ] **Step 2: 运行测试确认失败**
+
+```bash
+cd cfc-backend && mvn test -Dtest=DailyOverviewServiceTest
+```
+
+Expected: FAIL(`DailyOverviewService` 不存在,编译错误)。
+
+- [ ] **Step 3: 创建 DailyOverviewVO**
+
+创建 `cfc-backend/src/main/java/com/etotem/cfc/dto/DailyOverviewVO.java`:
+
+```java
+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
+    }
+}
+```
+
+- [ ] **Step 4: 创建 DailyOverviewService**
+
+创建 `cfc-backend/src/main/java/com/etotem/cfc/service/DailyOverviewService.java`:
+
+```java
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.dto.DailyOverviewVO;
+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.etotem.cfc.dto.DanReportUploadVO;
+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();
+
+        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();
+        Integer count = articleReadingRecordMapper.selectCount(
+                new LambdaQueryWrapper<ArticleReadingRecord>()
+                        .eq(ArticleReadingRecord::getChildId, memberId)
+                        .apply("DATE(read_at) = CURDATE()"));
+        s.setArticleReadToday(count == null ? 0 : count);
+        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;
+    }
+}
+```
+
+- [ ] **Step 5: 运行测试确认通过**
+
+```bash
+cd cfc-backend && mvn test -Dtest=DailyOverviewServiceTest
+```
+
+Expected: PASS(若个别 field 名与步骤 0 校验不符,按步骤 0 结论修正引用后重跑)。
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/DailyOverviewVO.java \
+        cfc-backend/src/main/java/com/etotem/cfc/service/DailyOverviewService.java \
+        cfc-backend/src/test/java/com/etotem/cfc/service/DailyOverviewServiceTest.java
+git commit -m "feat(backend): 新增今日任务聚合服务 DailyOverviewService + DailyOverviewVO
+- 聚合打卡/任务/阅读活动/测评报告四类今日状态
+- 纯只读,全部复用现有 service/mapper 查询逻辑
+- 单元测试 DailyOverviewServiceTest(@SpringBootTest + @MockBean)"
+```
+
+---
+
+### Task 2: 后端聚合接口 — DailyTaskController
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/DailyTaskController.java`
+- Test: `cfc-backend/src/test/java/com/etotem/cfc/controller/DailyTaskControllerTest.java`
+
+**Interfaces:**
+- Consumes: `DailyOverviewService.aggregate(userId, memberId, familyId)`(Task 1)
+- Produces: `POST /api/daily-task/overview` → `Result<DailyOverviewVO>`(Task 3 前端调用)
+
+- [ ] **Step 1: 前置检查(AGENTS.md 强制)**
+
+```bash
+cd cfc-backend && mvn clean compile
+# 检查路由重复(确认 /api/daily-task/overview 未被占用)
+grep -rn '@Mapping' cfc-backend/src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u | Select-String 'daily-task'
+# 检查 Bean 命名冲突(确认无同名 DailyTaskController/DailyOverviewService)
+Get-ChildItem -Recurse cfc-backend/src/main/java -Filter 'DailyTaskController.java' | Select-Object FullName
+Get-ChildItem -Recurse cfc-backend/src/main/java -Filter 'DailyOverviewService.java' | Select-Object FullName
+```
+
+Expected: 编译通过;路由无占用;Bean 无重名。
+
+- [ ] **Step 2: 写失败测试**
+
+创建 `cfc-backend/src/test/java/com/etotem/cfc/controller/DailyTaskControllerTest.java`:
+
+```java
+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());
+    }
+}
+```
+
+- [ ] **Step 3: 运行测试确认失败**
+
+```bash
+cd cfc-backend && mvn test -Dtest=DailyTaskControllerTest
+```
+
+Expected: FAIL(`DailyTaskController` 不存在)。
+
+- [ ] **Step 4: 创建 DailyTaskController**
+
+创建 `cfc-backend/src/main/java/com/etotem/cfc/controller/DailyTaskController.java`(参考 TaskController.getTodayTasks 与 DailyCheckinController.list 的骨架):
+
+```java
+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));
+    }
+}
+```
+
+- [ ] **Step 5: 运行测试确认通过**
+
+```bash
+cd cfc-backend && mvn test -Dtest=DailyTaskControllerTest
+```
+
+Expected: PASS。
+
+- [ ] **Step 6: 全量验证**
+
+```bash
+cd cfc-backend && mvn clean compile && mvn test
+```
+
+Expected: 编译通过 + 全部测试通过(含既有 70 个测试文件的回归)。
+
+- [ ] **Step 7: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/DailyTaskController.java \
+        cfc-backend/src/test/java/com/etotem/cfc/controller/DailyTaskControllerTest.java
+git commit -m "feat(backend): 新增今日任务聚合接口 POST /api/daily-task/overview
+- memberId 回退链:请求体 memberId → currentMemberId → userId(成长任务)
+- 统一 @PostMapping + Result<T>,只读不写库
+- 单元测试 DailyTaskControllerTest"
+```
+
+---
+
+### Task 3: 前端 API 封装 + 今日任务卡片组件
+
+**Files:**
+- Modify: `cfc-frontend/utils/api.js`(末尾追加导出函数)
+- Create: `cfc-frontend/components/daily-task-card.vue`
+- Test: `node --check`(语法校验,AGENTS.md:禁止 npm build)
+
+**Interfaces:**
+- Consumes: 后端 `POST /api/daily-task/overview`(Task 2),响应 `DailyOverviewVO` 结构
+- Produces: `getDailyTaskOverview()`(api.js)、`<DailyTaskCard />` 组件(Task 4 挂载首页)
+
+- [ ] **Step 1: api.js 追加导出函数**
+
+在 `cfc-frontend/utils/api.js` 末尾追加(参考现有 `getTodayTasks` 第 463-465 行风格):
+
+```js
+/**
+ * 今日任务总览(首页卡片聚合:打卡/任务/阅读活动/测评报告)
+ */
+export const getDailyTaskOverview = () => {
+  return request('/api/daily-task/overview', 'POST', {})
+}
+```
+
+- [ ] **Step 2: 创建 daily-task-card.vue**
+
+创建 `cfc-frontend/components/daily-task-card.vue`(参考 ProfileGrowth.vue 的 onShow 拉数据 + uni.navigateTo 模式):
+
+```vue
+<template>
+  <view class="daily-task-card" v-if="overview">
+    <view class="daily-task-head">
+      <text class="daily-task-title">今日任务</text>
+      <text class="daily-task-date">{{ overview.date }}</text>
+    </view>
+
+    <view v-if="loading" class="daily-task-state">
+      <text class="daily-task-state-text">加载中...</text>
+    </view>
+
+    <block v-else-if="error">
+      <view class="daily-task-state" @click="loadOverview">
+        <text class="daily-task-state-text">加载失败,点击重试</text>
+      </view>
+    </block>
+
+    <block v-else>
+      <!-- 打卡类 -->
+      <view class="daily-task-group" v-if="overview.checkins && overview.checkins.length > 0">
+        <view class="daily-task-group-title">打卡</view>
+        <view class="daily-task-item" v-for="item in overview.checkins" :key="getCheckinKey(item)" @click="go(item.route)">
+          <text class="daily-task-item-label">{{ item.label }}</text>
+          <text v-if="item.done" class="daily-task-done">✓</text>
+          <text v-else class="daily-task-go">去完成</text>
+        </view>
+      </view>
+
+      <!-- 任务类 -->
+      <view class="daily-task-group" v-if="overview.tasks">
+        <view class="daily-task-group-title">任务</view>
+        <view class="daily-task-item" @click="go(overview.tasks.route)">
+          <text class="daily-task-item-label">待完成 {{ overview.tasks.todoToday }} · 已完成 {{ overview.tasks.doneToday }}</text>
+          <text v-if="overview.tasks.todoToday > 0" class="daily-task-go">去完成</text>
+          <text v-else class="daily-task-done">✓</text>
+        </view>
+      </view>
+
+      <!-- 阅读活动 -->
+      <view class="daily-task-group" v-if="overview.activities">
+        <view class="daily-task-group-title">阅读活动</view>
+        <view class="daily-task-item" @click="go(overview.activities.articleRoute)">
+          <text class="daily-task-item-label">今日已读 {{ overview.activities.articleReadToday }} 篇</text>
+          <text v-if="overview.activities.articleReadToday > 0" class="daily-task-done">✓</text>
+          <text v-else class="daily-task-go">去阅读</text>
+        </view>
+      </view>
+
+      <!-- 测评报告 -->
+      <view class="daily-task-group" v-if="overview.assessments">
+        <view class="daily-task-group-title">测评报告</view>
+        <view class="daily-task-item" @click="go(overview.assessments.route)">
+          <text class="daily-task-item-label">进行中 {{ overview.assessments.ongoing }} · 待处理 {{ overview.assessments.reportPending }}</text>
+          <text v-if="overview.assessments.ongoing > 0 || overview.assessments.reportPending > 0" class="daily-task-go">去处理</text>
+          <text v-else class="daily-task-done">✓</text>
+        </view>
+      </view>
+
+      <!-- 全部完成空态 -->
+      <view class="daily-task-all" v-if="allDone()">
+        <text class="daily-task-all-text">今日任务已全部完成 🎉</text>
+      </view>
+    </block>
+  </view>
+</template>
+
+<script>
+import { getDailyTaskOverview } from '../utils/api.js'
+
+export default {
+  name: 'DailyTaskCard',
+  data() {
+    return {
+      overview: null,
+      loading: false,
+      error: false
+    }
+  },
+  onShow() {
+    if (!uni.getStorageSync('token')) return
+    this.loadOverview()
+  },
+  methods: {
+    loadOverview: function() {
+      var self = this
+      self.loading = true
+      self.error = false
+      getDailyTaskOverview().then(function(res) {
+        self.loading = false
+        if (res.code === 200 && res.data) {
+          self.overview = res.data
+        } else {
+          self.overview = null
+          self.error = true
+        }
+      }).catch(function() {
+        self.loading = false
+        self.overview = null
+        self.error = true
+      })
+    },
+    getCheckinKey: function(item) {
+      return item.key || item.label || ''
+    },
+    go: function(route) {
+      if (!route) return
+      uni.navigateTo({ url: route })
+    },
+    allDone: function() {
+      var o = this.overview
+      if (!o) return false
+      var all = true
+      if (o.checkins) {
+        var i
+        for (i = 0; i < o.checkins.length; i++) {
+          if (!o.checkins[i].done) all = false
+        }
+      }
+      if (o.tasks && o.tasks.todoToday > 0) all = false
+      if (o.activities && o.activities.articleReadToday === 0) all = false
+      if (o.assessments && (o.assessments.ongoing > 0 || o.assessments.reportPending > 0)) all = false
+      return all
+    }
+  }
+}
+</script>
+
+<style scoped>
+.daily-task-card {
+  margin: 24rpx 24rpx 0;
+  padding: 28rpx;
+  border-radius: 24rpx;
+  background: #FFFFFF;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
+}
+.daily-task-head {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20rpx;
+}
+.daily-task-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #1F2937;
+}
+.daily-task-date {
+  font-size: 24rpx;
+  color: #9CA3AF;
+}
+.daily-task-state {
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  padding: 24rpx 0;
+}
+.daily-task-state-text {
+  font-size: 26rpx;
+  color: #9CA3AF;
+}
+.daily-task-group {
+  display: flex;
+  flex-direction: column;
+  margin-bottom: 16rpx;
+}
+.daily-task-group-title {
+  font-size: 24rpx;
+  color: #9CA3AF;
+  margin-bottom: 8rpx;
+}
+.daily-task-item {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  padding: 18rpx 0;
+  border-bottom: 1rpx solid #F3F4F6;
+}
+.daily-task-item:last-child {
+  border-bottom: none;
+}
+.daily-task-item-label {
+  font-size: 28rpx;
+  color: #374151;
+}
+.daily-task-go {
+  font-size: 26rpx;
+  color: #F97316;
+  font-weight: 500;
+}
+.daily-task-done {
+  font-size: 26rpx;
+  color: #10B981;
+  font-weight: 600;
+}
+.daily-task-all {
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  padding: 12rpx 0;
+}
+.daily-task-all-text {
+  font-size: 28rpx;
+  color: #10B981;
+  font-weight: 500;
+}
+</style>
+```
+
+- [ ] **Step 3: 语法校验**
+
+```bash
+# 提取 script 块做 node --check(AGENTS.md:不打包)
+node -e "const fs=require('fs');const s=fs.readFileSync('cfc-frontend/components/daily-task-card.vue','utf8');const m=s.match(/<script>([\s\S]*?)<\/script>/);if(!m){throw new Error('no script block')};new Function(m[1].replace(/import .*?from ['\"][^'\"]+['\"];?\s*/g,'').replace(/export default/,'const __comp ='));console.log('syntax OK')"
+```
+
+Expected: 输出 `syntax OK`(`new Function` 只做语法解析不执行)。
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-frontend/utils/api.js cfc-frontend/components/daily-task-card.vue
+git commit -m "feat(frontend): 新增今日任务卡片组件 daily-task-card.vue + 聚合接口封装
+- utils/api.js 新增 getDailyTaskOverview()
+- 组件自带 onShow 拉取刷新,点击项 uni.navigateTo 跳转
+- BaseLoading/BaseEmpty 风格与小程序限制(无 ?. 可选链等)全部遵从"
+```
+
+---
+
+### Task 4: 首页集成 — 挂载卡片到 index-home
+
+**Files:**
+- Modify: `cfc-frontend/pages/index-home/index.vue`(import 第 347 行附近、components 注册第 353-362 行、模板第 61-77 行之间)
+
+**Interfaces:**
+- Consumes: `<DailyTaskCard />` 组件(Task 3)
+- Produces: 首页登录态(`v-if="currentRole"` 容器内)渲染今日任务卡片
+
+- [ ] **Step 1: import**
+
+在 `cfc-frontend/pages/index-home/index.vue` 第 347 行(`import JourneyCard from '../../components/journey-card.vue'`)后追加:
+
+```js
+import DailyTaskCard from '../../components/daily-task-card.vue'
+```
+
+- [ ] **Step 2: components 注册**
+
+在第 353-362 行的 `components: { ... }` 对象中追加:
+
+```js
+DailyTaskCard,
+```
+
+(放在 `JourneyCard` 后,保持字母序)
+
+- [ ] **Step 3: 模板挂载**
+
+在模板第 64 行 `</JourneyCard>` 之后、第 67 行 `<WuxingSandbox` 之前插入:
+
+```html
+    <!-- 今日任务卡片 — 每日任务统一入口 -->
+    <DailyTaskCard />
+```
+
+- [ ] **Step 4: 语法校验**
+
+```bash
+node -e "const fs=require('fs');const s=fs.readFileSync('cfc-frontend/pages/index-home/index.vue','utf8');const m=s.match(/<script>([\s\S]*?)<\/script>/);if(!m){throw new Error('no script block')};new Function(m[1].replace(/import .*?from ['\"][^'\"]+['\"];?\s*/g,'').replace(/export default/,'const __comp ='));console.log('syntax OK')"
+```
+
+Expected: 输出 `syntax OK`。
+
+- [ ] **Step 5: 确认模板顺序**
+
+```bash
+Select-String -Path cfc-frontend/pages/index-home/index.vue -Pattern 'DailyTaskCard|JourneyCard|WuxingSandbox'
+```
+
+Expected: `DailyTaskCard` 出现在 import/components/模板三处,且模板中位于 JourneyCard 与 WuxingSandbox 之间。
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add cfc-frontend/pages/index-home/index.vue
+git commit -m "feat(frontend): 首页集成今日任务卡片(JourneyCard 与 WuxingSandbox 之间)"
+```
+
+---
+
+## 验收核对(对应 spec §7)
+
+| spec 验收标准 | 验证 task/step |
+|---|---|
+| 首页登录态渲染卡片,未登录不显示 | Task 4 Step 3(`v-if="currentRole"` 容器内)+ 组件 onShow 无 token 直接 return |
+| 打卡/任务/阅读/测评四分组正确展示 | Task 1 Step 4(buildCheckins/buildTasks/buildActivities/buildAssessments) |
+| 点击动作项跳转到对应功能页 | Task 1 VO 下发 route + Task 3 `go()` 方法 |
+| 完成操作返回后卡片自动刷新 ✓ | Task 3 组件 `onShow()` 每次拉取(避开 index-home `loggedInInitDone` 单次守卫) |
+| 聚合接口只读,零写操作 | Task 1(纯查询)+ Task 2(只调 aggregate) |
+| memberId 回退链正确 | Task 2 Step 4(body → currentMemberId → aggregate 内 userId 用于成长任务) |
+| 接口失败降级 + 重试 | Task 3 `error` 态 + 点击重试 |
+| `node --check` 语法校验,不打包 | Task 3 Step 3 / Task 4 Step 4 |