|
@@ -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 |
|