|
|
@@ -0,0 +1,855 @@
|
|
|
+# 管理端家庭信息查看 + 家庭命名「XX家庭」实现计划
|
|
|
+
|
|
|
+> **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:** 在管理端家庭列表增加「家庭相关信息查看」(报告/方案/任务,计数+可下钻明细),并实现家庭主账号填真实姓名后家庭名自动改为「XX家庭」。
|
|
|
+
|
|
|
+**Architecture:** 后端新增 `AdminFamilyOverviewController`(`/api/admin/families/{id}/overview` + `/tasks`)+ `FamilyAdminOverviewService`,计数用 `selectMaps+groupBy`、任务明细走分页;前端在 `Families.vue`「更多」下拉加「查看信息」→ 新建 `FamilyOverviewDialog.vue`(计数卡片+三 tab 明细)。家庭命名:`FamilyNamingSupport.buildFamilyName` 静态方法,创建家庭与更新资料两处调用,存量一次性迁移回填。
|
|
|
+
|
|
|
+**Tech Stack:** Java 17 / Spring Boot 2.7.18 / MyBatis-Plus 3.5.3.1 / Lombok / JUnit5+Mockito(`@SpringBootTest`+`@MockBean`);前端 Vue2 + Element UI 2.15.13。
|
|
|
+
|
|
|
+**Spec:** `docs/superpowers/specs/2026-10-30-family-overview-naming-design.md`
|
|
|
+
|
|
|
+## Global Constraints
|
|
|
+
|
|
|
+- 后端鉴权沿用 `@RequestAttribute("role") String role` + `if (!"admin".equals(role)) return Result.error("无权限")`(`JwtInterceptor` 已设置该属性)。
|
|
|
+- MyBatis-Plus 3.5.3.1:`selectCount` 返回 `Long`;**分组计数必须用 `selectMaps` + `groupBy`**(禁用 `selectCount` 做分组)。
|
|
|
+- 计数字段类型用 `long`(非 `int`)。
|
|
|
+- 报告/方案明细全量返回(上限 8/4),任务明细走独立分页接口(默认 20/页、上限 100)。
|
|
|
+- 方案**不返回** `plan_content` / `plan_json` / `member_ids` / `dimensions` / `review_comment`(响应体裁剪)。
|
|
|
+- 命名规则:`real_name` 非空 → `realName.trim() + "家庭"`;否则回退 `nickname + "的家庭"`;nickname 空 → `"我的家庭"`。
|
|
|
+- 命名迁移放 `DatabaseInitializer.runMigrations()` 新迁移(幂等),执行前 dry-run 报行数给主理人确认。
|
|
|
+- `creator_id` 为 NULL → overview 主账号字段返回空串;迁移 INNER JOIN 跳过。
|
|
|
+- 前端复用 `StatusTag`(`value+options` 映射式)、`StatCard`(`clickable`+`@click`)、`SectionTitle`。
|
|
|
+
|
|
|
+## Review Focus
|
|
|
+
|
|
|
+1. **非 admin 角色访问 overview/tasks** → 应返回 `Result.error("无权限")` 而非抛异常(Task 5 测试覆盖)。
|
|
|
+2. **不存在的 familyId** → overview 应返回 `Result.error("家庭不存在")`,tasks 返回空页而非报错(Task 5 测试覆盖)。
|
|
|
+3. **报告 status 全量展示**(active/deleted/archived)→ 管理端需能看到已归档报告(Task 4 服务测试断言 count 含 archived)。
|
|
|
+4. **任务数超单页** → 分页正确返回 `total` 与当前页(Task 4/5 测试覆盖)。
|
|
|
+5. **real_name 为空/全空格、nickname 为空** → 命名回退链正确(Task 1 测试覆盖)。
|
|
|
+6. **真实姓名变更时家庭名联动** → 仅当 realName 非空且与原值不同时改家庭名(Task 2 测试覆盖)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: FamilyNamingSupport 工具类(命名规则,TDD)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/util/FamilyNamingSupport.java`
|
|
|
+- Test: `cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyNamingSupportTest.java`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Produces: `static String buildFamilyName(User user)` — 输入 `User`(有 `realName`、`nickname`),返回家庭名。
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败测试** `FamilyNamingSupportTest.java`
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
+import com.etotem.cfc.util.FamilyNamingSupport;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+
|
|
|
+public class FamilyNamingSupportTest {
|
|
|
+ private User user(String realName, String nickname) {
|
|
|
+ User u = new User();
|
|
|
+ u.setRealName(realName);
|
|
|
+ u.setNickname(nickname);
|
|
|
+ return u;
|
|
|
+ }
|
|
|
+ @Test void realName_优先() { assertEquals("张三家庭", FamilyNamingSupport.buildFamilyName(user("张三", "昵称"))); }
|
|
|
+ @Test void realName空_回退昵称() { assertEquals("昵称的家庭", FamilyNamingSupport.buildFamilyName(user("", "昵称"))); }
|
|
|
+ @Test void realName全空格_回退昵称() { assertEquals("昵称的家庭", FamilyNamingSupport.buildFamilyName(user(" ", "昵称"))); }
|
|
|
+ @Test void 都空_我的家庭() { assertEquals("我的家庭", FamilyNamingSupport.buildFamilyName(user(null, null))); }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 运行测试确认失败**
|
|
|
+
|
|
|
+Run: `cd cfc-backend && mvn -q test -Dtest=FamilyNamingSupportTest -DfailIfNoTests=false`
|
|
|
+Expected: FAIL(`FamilyNamingSupport` 不存在,编译失败)
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现** `FamilyNamingSupport.java`
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.util;
|
|
|
+
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
+
|
|
|
+/** 家庭命名规则:真实姓名优先 → 昵称 → 兜底 */
|
|
|
+public final class FamilyNamingSupport {
|
|
|
+ private FamilyNamingSupport() {}
|
|
|
+ public static String buildFamilyName(User user) {
|
|
|
+ if (user.getRealName() != null && !user.getRealName().trim().isEmpty()) {
|
|
|
+ return user.getRealName().trim() + "家庭";
|
|
|
+ }
|
|
|
+ String nickname = user.getNickname();
|
|
|
+ return (nickname != null && !nickname.isEmpty()) ? nickname + "的家庭" : "我的家庭";
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 运行测试确认通过**
|
|
|
+
|
|
|
+Run: `cd cfc-backend && mvn -q test -Dtest=FamilyNamingSupportTest -DfailIfNoTests=false`
|
|
|
+Expected: PASS(4/4)
|
|
|
+
|
|
|
+- [ ] **Step 5: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/util/FamilyNamingSupport.java cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyNamingSupportTest.java
|
|
|
+git commit -m "feat(family): 家庭命名规则工具 buildFamilyName"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: 创建家庭与更新资料两处调用新命名规则
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java:912-921`(`createFamilyForUser`)
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java:1159-1273`(`updateUserInfo`)
|
|
|
+- Test: `cfc-backend/src/test/java/com/etotem/cfc/service/UserServiceFamilyNameLinkTest.java`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Consumes: `FamilyNamingSupport.buildFamilyName(User)`(Task 1)
|
|
|
+- Produces: `updateUserInfo` 行为变更:真实姓名变化时同步家庭名。
|
|
|
+
|
|
|
+- [ ] **Step 1: 改 `createFamilyForUser` 命名**
|
|
|
+
|
|
|
+将 `FamilyMemberService.createFamilyForUser` 中:
|
|
|
+```java
|
|
|
+String familyName = (user.getNickname() != null && !user.getNickname().isEmpty())
|
|
|
+ ? user.getNickname() + "的家庭" : "我的家庭";
|
|
|
+family.setName(familyName);
|
|
|
+```
|
|
|
+改为:
|
|
|
+```java
|
|
|
+family.setName(FamilyNamingSupport.buildFamilyName(user));
|
|
|
+```
|
|
|
+(顶部 import `com.etotem.cfc.util.FamilyNamingSupport`)
|
|
|
+
|
|
|
+- [ ] **Step 2: 改 `updateUserInfo` 联动**
|
|
|
+
|
|
|
+在 `updateUserInfo` 方法开头 `User user = userMapper.selectById(userId);` 之后记录:
|
|
|
+```java
|
|
|
+String oldRealName = user.getRealName();
|
|
|
+```
|
|
|
+在 `userMapper.updateById(user);`(约 1273 行)之后、`syncUserConsignee` 之前插入:
|
|
|
+```java
|
|
|
+// 家庭名联动:主账号真实姓名变化时同步为「XX家庭」(非关键字段,失败仅告警)
|
|
|
+if (dto.getRealName() != null && !dto.getRealName().trim().isEmpty()
|
|
|
+ && !dto.getRealName().trim().equals(oldRealName == null ? "" : oldRealName.trim())) {
|
|
|
+ try {
|
|
|
+ LambdaQueryWrapper<Family> fw = new LambdaQueryWrapper<>();
|
|
|
+ fw.eq(Family::getCreatorId, userId);
|
|
|
+ Family ownedFamily = familyMapper.selectOne(fw);
|
|
|
+ if (ownedFamily != null) {
|
|
|
+ ownedFamily.setName(dto.getRealName().trim() + "家庭");
|
|
|
+ ownedFamily.setUpdatedAt(new Date());
|
|
|
+ familyMapper.updateById(ownedFamily);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("同步家庭名为真实姓名失败: userId={}, realName={}", userId, dto.getRealName(), e);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+(`UserService` 已 import `FamilyMapper`/`Family`;确认 `log` 已存在,否则加 `@Slf4j`)
|
|
|
+
|
|
|
+- [ ] **Step 3: 写测试** `UserServiceFamilyNameLinkTest.java`(`@SpringBootTest`+`@MockBean`)
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.etotem.cfc.entity.Family;
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
+import com.etotem.cfc.mapper.FamilyMapper;
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
+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 static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.Mockito.*;
|
|
|
+
|
|
|
+@SpringBootTest
|
|
|
+public class UserServiceFamilyNameLinkTest {
|
|
|
+ @Resource private UserService userService;
|
|
|
+ @MockBean private UserMapper userMapper;
|
|
|
+ @MockBean private FamilyMapper familyMapper;
|
|
|
+
|
|
|
+ @Test void realName变化_联动家庭名() {
|
|
|
+ User u = new User(); u.setId(10L); u.setRole("parent"); u.setRealName("旧名"); u.setFamilyId(7L);
|
|
|
+ when(userMapper.selectById(10L)).thenReturn(u);
|
|
|
+ Family f = new Family(); f.setId(7L); f.setName("旧名家庭"); f.setCreatorId(10L);
|
|
|
+ when(familyMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(f);
|
|
|
+ when(userMapper.updateById(any())).thenReturn(1);
|
|
|
+
|
|
|
+ com.etotem.cfc.dto.UpdateUserDTO dto = new com.etotem.cfc.dto.UpdateUserDTO();
|
|
|
+ dto.setRealName("王五");
|
|
|
+ userService.updateUserInfo(10L, dto);
|
|
|
+
|
|
|
+ verify(familyMapper).updateById(argThat(fam -> "王五家庭".equals(fam.getName())));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test void realName未变_不改家庭名() {
|
|
|
+ User u = new User(); u.setId(10L); u.setRole("parent"); u.setRealName("王五");
|
|
|
+ when(userMapper.selectById(10L)).thenReturn(u);
|
|
|
+ when(userMapper.updateById(any())).thenReturn(1);
|
|
|
+
|
|
|
+ com.etotem.cfc.dto.UpdateUserDTO dto = new com.etotem.cfc.dto.UpdateUserDTO();
|
|
|
+ dto.setRealName("王五");
|
|
|
+ userService.updateUserInfo(10L, dto);
|
|
|
+
|
|
|
+ verify(familyMapper, never()).updateById(any());
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> 注:`updateUserInfo` 可能调用 `syncUserConsignee`(需 `@MockBean` UserAddressMapper 等)——若测试环境缺 bean,用 `@MockBean` 补齐报错提示的依赖,或对 `syncUserConsignee` 打桩。测试以"能验证家庭名联动断言"为最小目标。
|
|
|
+
|
|
|
+- [ ] **Step 4: 运行测试确认通过**
|
|
|
+
|
|
|
+Run: `cd cfc-backend && mvn -q test -Dtest=UserServiceFamilyNameLinkTest -DfailIfNoTests=false`
|
|
|
+Expected: PASS
|
|
|
+
|
|
|
+- [ ] **Step 5: 运行既有 UserService 相关测试确认无回归**
|
|
|
+
|
|
|
+Run: `cd cfc-backend && mvn -q test -Dtest=UserServiceEffectiveFamilyIdsTest -DfailIfNoTests=false`
|
|
|
+Expected: PASS
|
|
|
+
|
|
|
+- [ ] **Step 6: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java cfc-backend/src/test/java/com/etotem/cfc/service/UserServiceFamilyNameLinkTest.java
|
|
|
+git commit -m "feat(family): 创建/更新资料时按真实姓名联动家庭名"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: 后端 DTO / VO 类(家庭信息查看)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyOverviewDTO.java`
|
|
|
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyOverviewCounts.java`
|
|
|
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyReportItemVO.java`
|
|
|
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyPlanItemVO.java`
|
|
|
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/TaskItemVO.java`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Produces: `FamilyOverviewDTO`(`family`、`creatorName`、`creatorRealName`、`counts:FamilyOverviewCounts`、`reports:List<FamilyReportItemVO>`、`plans:List<FamilyPlanItemVO>`);`FamilyOverviewCounts`(`long reports,plans,tasks` + `Map<String,Integer> reportTypeCounts,taskStatusCounts`);三个 ItemVO 字段见下。
|
|
|
+
|
|
|
+- [ ] **Step 1: 创建 5 个类(Lombok `@Data`,字段与设计文档 5.1 一致)**
|
|
|
+
|
|
|
+```java
|
|
|
+// FamilyReportItemVO.java
|
|
|
+@Data public class FamilyReportItemVO {
|
|
|
+ private Long id; private String reportType; private String reportNumber;
|
|
|
+ private java.util.Date reportDate; private String personName; private Long subjectId;
|
|
|
+ private String status; private Integer overallScore; private Integer gutHealthScore;
|
|
|
+ private java.util.Date createdAt;
|
|
|
+}
|
|
|
+// FamilyPlanItemVO.java
|
|
|
+@Data public class FamilyPlanItemVO {
|
|
|
+ private Long id; private String planType; private String memberName; private String goal;
|
|
|
+ private String status; private Integer totalDays; private Long teacherId; private java.util.Date createdAt;
|
|
|
+}
|
|
|
+// TaskItemVO.java
|
|
|
+@Data public class TaskItemVO {
|
|
|
+ private Long id; private String title; private String status; private String category;
|
|
|
+ private Integer points; private java.util.Date deadline; private Long childId;
|
|
|
+ private String executorType; private String taskType; private String sourceType;
|
|
|
+ private java.util.Date createdAt; private java.util.Date completedAt;
|
|
|
+}
|
|
|
+// FamilyOverviewCounts.java
|
|
|
+@Data public class FamilyOverviewCounts {
|
|
|
+ private long reports; private long plans; private long tasks;
|
|
|
+ private java.util.Map<String, Integer> reportTypeCounts = new java.util.HashMap<>();
|
|
|
+ private java.util.Map<String, Integer> taskStatusCounts = new java.util.HashMap<>();
|
|
|
+}
|
|
|
+// FamilyOverviewDTO.java
|
|
|
+@Data public class FamilyOverviewDTO {
|
|
|
+ private java.util.Map<String, Object> family;
|
|
|
+ private String creatorName; private String creatorRealName;
|
|
|
+ private FamilyOverviewCounts counts;
|
|
|
+ private java.util.List<FamilyReportItemVO> reports;
|
|
|
+ private java.util.List<FamilyPlanItemVO> plans;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 编译确认**
|
|
|
+
|
|
|
+Run: `cd cfc-backend && mvn -q compile -DskipTests`
|
|
|
+Expected: BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **Step 3: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyOverviewDTO.java cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyOverviewCounts.java cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyReportItemVO.java cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyPlanItemVO.java cfc-backend/src/main/java/com/etotem/cfc/dto/TaskItemVO.java
|
|
|
+git commit -m "feat(family): 家庭信息查看 DTO/VO"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: FamilyAdminOverviewService(计数+明细,TDD)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/FamilyAdminOverviewService.java`
|
|
|
+- Test: `cfc-backend/src/test/java/com/etotem/cfc/service/FamilyAdminOverviewServiceTest.java`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Consumes: DTO/VO(Task 3)
|
|
|
+- Produces: `FamilyOverviewDTO buildOverview(Long familyId)`;`com.baomidou.mybatisplus.extension.plugins.pagination.Page<TaskItemVO> pageTasks(Long familyId, long page, long size)`。
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败测试** `FamilyAdminOverviewServiceTest.java`(`@SpringBootTest`+`@MockBean` 五个 mapper)
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.etotem.cfc.dto.FamilyOverviewDTO;
|
|
|
+import com.etotem.cfc.entity.Family;
|
|
|
+import com.etotem.cfc.mapper.*;
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
+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.any;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+@SpringBootTest
|
|
|
+public class FamilyAdminOverviewServiceTest {
|
|
|
+ @Resource private FamilyAdminOverviewService svc;
|
|
|
+ @MockBean private FamilyMapper familyMapper;
|
|
|
+ @MockBean private HealthReportMapper reportMapper;
|
|
|
+ @MockBean private HealthPlanMapper planMapper;
|
|
|
+ @MockBean private TaskMapper taskMapper;
|
|
|
+ @MockBean private UserMapper userMapper;
|
|
|
+
|
|
|
+ @Test void overview_计数与明细() {
|
|
|
+ Family f = new Family(); f.setId(7L); f.setName("王五家庭"); f.setCreatorId(10L);
|
|
|
+ when(familyMapper.selectById(7L)).thenReturn(f);
|
|
|
+ when(reportMapper.selectCount(any())).thenReturn(8L);
|
|
|
+ when(planMapper.selectCount(any())).thenReturn(4L);
|
|
|
+ when(taskMapper.selectCount(any())).thenReturn(346L);
|
|
|
+ // 分组计数用 selectMaps(返回 [{k,v}])
|
|
|
+ when(reportMapper.selectMaps(any())).thenReturn(Arrays.asList(map("gut_flora","5"), map("brain_status","3")));
|
|
|
+ when(taskMapper.selectMaps(any())).thenReturn(Arrays.asList(map("pending","340"), map("completed","6")));
|
|
|
+ when(reportMapper.selectList(any())).thenReturn(new ArrayList<>());
|
|
|
+ when(planMapper.selectList(any())).thenReturn(new ArrayList<>());
|
|
|
+
|
|
|
+ FamilyOverviewDTO dto = svc.buildOverview(7L);
|
|
|
+ assertEquals(8L, dto.getCounts().getReports());
|
|
|
+ assertEquals(4L, dto.getCounts().getPlans());
|
|
|
+ assertEquals(346L, dto.getCounts().getTasks());
|
|
|
+ assertEquals(Integer.valueOf(5), dto.getCounts().getReportTypeCounts().get("gut_flora"));
|
|
|
+ assertEquals(Integer.valueOf(340), dto.getCounts().getTaskStatusCounts().get("pending"));
|
|
|
+ assertNotNull(dto.getReports());
|
|
|
+ assertNotNull(dto.getPlans());
|
|
|
+ }
|
|
|
+ @Test void overview_家庭不存在() {
|
|
|
+ when(familyMapper.selectById(999L)).thenReturn(null);
|
|
|
+ assertThrows(RuntimeException.class, () -> svc.buildOverview(999L));
|
|
|
+ }
|
|
|
+ @Test void pageTasks_分页() {
|
|
|
+ when(taskMapper.selectPage(any(), any())).thenReturn(new Page<TaskItemVO>() {{
|
|
|
+ setTotal(346); setRecords(new ArrayList<>());
|
|
|
+ }});
|
|
|
+ Page<TaskItemVO> page = svc.pageTasks(7L, 1, 20);
|
|
|
+ assertEquals(346L, page.getTotal());
|
|
|
+ assertEquals(20L, page.getSize());
|
|
|
+ }
|
|
|
+ private Map<String,Object> map(String k, String v) { Map<String,Object> m = new HashMap<>(); m.put("k", k); m.put("v", Long.parseLong(v)); return m; }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> 注:若 `selectCount`/`selectList`/`selectPage` 的 mock 与 LambdaQueryWrapper 参数不匹配,改用 `any(LambdaQueryWrapper.class)` 或 `any(Wrapper.class)`。`selectMaps` 分组查询返回的 `v` 为 `Long`(COUNT)。
|
|
|
+
|
|
|
+- [ ] **Step 2: 运行确认失败**
|
|
|
+
|
|
|
+Run: `cd cfc-backend && mvn -q test -Dtest=FamilyAdminOverviewServiceTest -DfailIfNoTests=false`
|
|
|
+Expected: FAIL(service 不存在,编译失败)
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现** `FamilyAdminOverviewService.java`
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
+import com.etotem.cfc.dto.*;
|
|
|
+import com.etotem.cfc.entity.*;
|
|
|
+import com.etotem.cfc.mapper.*;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class FamilyAdminOverviewService {
|
|
|
+ @Resource private FamilyMapper familyMapper;
|
|
|
+ @Resource private HealthReportMapper reportMapper;
|
|
|
+ @Resource private HealthPlanMapper planMapper;
|
|
|
+ @Resource private TaskMapper taskMapper;
|
|
|
+ @Resource private UserMapper userMapper;
|
|
|
+
|
|
|
+ public FamilyOverviewDTO buildOverview(Long familyId) {
|
|
|
+ Family family = familyMapper.selectById(familyId);
|
|
|
+ if (family == null) throw new RuntimeException("家庭不存在");
|
|
|
+
|
|
|
+ FamilyOverviewDTO dto = new FamilyOverviewDTO();
|
|
|
+ Map<String,Object> f = new HashMap<>();
|
|
|
+ f.put("id", family.getId()); f.put("name", family.getName());
|
|
|
+ f.put("inviteCode", family.getInviteCode()); f.put("creatorId", family.getCreatorId());
|
|
|
+ f.put("createdAt", family.getCreatedAt());
|
|
|
+ f.put("province", family.getProvince()); f.put("city", family.getCity());
|
|
|
+ f.put("district", family.getDistrict()); f.put("street", family.getStreet());
|
|
|
+ dto.setFamily(f);
|
|
|
+
|
|
|
+ User creator = family.getCreatorId() != null ? userMapper.selectById(family.getCreatorId()) : null;
|
|
|
+ dto.setCreatorName(creator != null && creator.getNickname() != null ? creator.getNickname() : "");
|
|
|
+ dto.setCreatorRealName(creator != null && creator.getRealName() != null ? creator.getRealName() : "");
|
|
|
+
|
|
|
+ FamilyOverviewCounts counts = new FamilyOverviewCounts();
|
|
|
+ counts.setReports(reportMapper.selectCount(new LambdaQueryWrapper<HealthReport>().eq(HealthReport::getFamilyId, familyId)));
|
|
|
+ counts.setPlans(planMapper.selectCount(new LambdaQueryWrapper<HealthPlan>().eq(HealthPlan::getFamilyId, familyId)));
|
|
|
+ counts.setTasks(taskMapper.selectCount(new LambdaQueryWrapper<Task>().eq(Task::getFamilyId, familyId)));
|
|
|
+ counts.setReportTypeCounts(countBy(reportMapper, "report_type", "family_id", familyId));
|
|
|
+ counts.setTaskStatusCounts(countBy(taskMapper, "status", "family_id", familyId));
|
|
|
+ dto.setCounts(counts);
|
|
|
+
|
|
|
+ dto.setReports(reportMapper.selectList(new LambdaQueryWrapper<HealthReport>()
|
|
|
+ .eq(HealthReport::getFamilyId, familyId)
|
|
|
+ .orderByDesc(HealthReport::getReportDate).orderByDesc(HealthReport::getId)));
|
|
|
+ dto.setPlans(planMapper.selectList(new LambdaQueryWrapper<HealthPlan>()
|
|
|
+ .eq(HealthPlan::getFamilyId, familyId)
|
|
|
+ .orderByDesc(HealthPlan::getCreatedAt).orderByDesc(HealthPlan::getId)));
|
|
|
+ return dto;
|
|
|
+ }
|
|
|
+
|
|
|
+ private <T> Map<String,Integer> countBy(com.baomidou.mybatisplus.core.mapper.BaseMapper<T> mapper,
|
|
|
+ String groupCol, String familyCol, Long familyId) {
|
|
|
+ QueryWrapper<T> qw = new QueryWrapper<>();
|
|
|
+ qw.select(groupCol + " as k", "count(*) as v").eq(familyCol, familyId).groupBy(groupCol);
|
|
|
+ Map<String,Integer> out = new HashMap<>();
|
|
|
+ for (Map<String,Object> row : mapper.selectMaps(qw)) {
|
|
|
+ Object k = row.get("k"); Object v = row.get("v");
|
|
|
+ if (k != null && v != null) out.put(String.valueOf(k), ((Number) v).intValue());
|
|
|
+ }
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Page<TaskItemVO> pageTasks(Long familyId, long page, long size) {
|
|
|
+ long safeSize = Math.min(Math.max(size, 1), 100);
|
|
|
+ Page<Task> raw = taskMapper.selectPage(new Page<>(page, safeSize),
|
|
|
+ new LambdaQueryWrapper<Task>().eq(Task::getFamilyId, familyId)
|
|
|
+ .orderByDesc(Task::getCreatedAt).orderByDesc(Task::getId));
|
|
|
+ Page<TaskItemVO> out = new Page<>(raw.getCurrent(), raw.getSize(), raw.getTotal());
|
|
|
+ out.setRecords(new ArrayList<>());
|
|
|
+ for (Task t : raw.getRecords()) {
|
|
|
+ TaskItemVO vo = new TaskItemVO();
|
|
|
+ vo.setId(t.getId()); vo.setTitle(t.getTitle()); vo.setStatus(t.getStatus());
|
|
|
+ vo.setCategory(t.getCategory()); vo.setPoints(t.getPoints()); vo.setDeadline(t.getDeadline());
|
|
|
+ vo.setChildId(t.getChildId()); vo.setExecutorType(t.getExecutorType());
|
|
|
+ vo.setTaskType(t.getTaskType()); vo.setSourceType(t.getSourceType());
|
|
|
+ vo.setCreatedAt(t.getCreatedAt()); vo.setCompletedAt(t.getCompletedAt());
|
|
|
+ out.getRecords().add(vo);
|
|
|
+ }
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 运行测试确认通过**
|
|
|
+
|
|
|
+Run: `cd cfc-backend && mvn -q test -Dtest=FamilyAdminOverviewServiceTest -DfailIfNoTests=false`
|
|
|
+Expected: PASS(3/3)
|
|
|
+
|
|
|
+- [ ] **Step 5: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/service/FamilyAdminOverviewService.java cfc-backend/src/test/java/com/etotem/cfc/service/FamilyAdminOverviewServiceTest.java
|
|
|
+git commit -m "feat(family): 家庭信息查看服务(计数+报告方案全量+任务分页)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 5: AdminFamilyOverviewController(接口,TDD)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminFamilyOverviewController.java`
|
|
|
+- Test: `cfc-backend/src/test/java/com/etotem/cfc/integration/controller/AdminFamilyOverviewControllerTest.java`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Consumes: `FamilyAdminOverviewService`(Task 4)
|
|
|
+- Produces: `POST /api/admin/families/{id}/overview`、`POST /api/admin/families/{id}/tasks`。
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败测试**(`@SpringBootTest`+`@MockBean` service)
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.integration.controller;
|
|
|
+
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
+import com.etotem.cfc.controller.admin.AdminFamilyOverviewController;
|
|
|
+import com.etotem.cfc.dto.FamilyOverviewDTO;
|
|
|
+import com.etotem.cfc.service.FamilyAdminOverviewService;
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
+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 static org.junit.jupiter.api.Assertions.*;
|
|
|
+import static org.mockito.ArgumentMatchers.*;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+@SpringBootTest
|
|
|
+public class AdminFamilyOverviewControllerTest {
|
|
|
+ @Resource private AdminFamilyOverviewController ctrl;
|
|
|
+ @MockBean private FamilyAdminOverviewService svc;
|
|
|
+
|
|
|
+ @Test void overview_非admin拒绝() {
|
|
|
+ Result<FamilyOverviewDTO> r = ctrl.getOverview(7L, "parent");
|
|
|
+ assertTrue(r.getCode() != 200);
|
|
|
+ }
|
|
|
+ @Test void overview_成功() {
|
|
|
+ when(svc.buildOverview(7L)).thenReturn(new FamilyOverviewDTO());
|
|
|
+ Result<FamilyOverviewDTO> r = ctrl.getOverview(7L, "admin");
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
+ assertNotNull(r.getData());
|
|
|
+ }
|
|
|
+ @Test void tasks_分页() {
|
|
|
+ when(svc.pageTasks(eq(7L), anyLong(), anyLong())).thenReturn(new Page<>());
|
|
|
+ Result<Page<com.etotem.cfc.dto.TaskItemVO>> r = ctrl.getTasks(7L, 1L, 20L, "admin");
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 运行确认失败**
|
|
|
+
|
|
|
+Run: `cd cfc-backend && mvn -q test -Dtest=AdminFamilyOverviewControllerTest -DfailIfNoTests=false`
|
|
|
+Expected: FAIL(controller 不存在)
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现** `AdminFamilyOverviewController.java`
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.controller.admin;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
+import com.etotem.cfc.dto.FamilyOverviewDTO;
|
|
|
+import com.etotem.cfc.dto.TaskItemVO;
|
|
|
+import com.etotem.cfc.service.FamilyAdminOverviewService;
|
|
|
+import io.swagger.v3.oas.annotations.Operation;
|
|
|
+import io.swagger.v3.oas.annotations.tags.Tag;
|
|
|
+import org.springframework.web.bind.annotation.*;
|
|
|
+import javax.annotation.Resource;
|
|
|
+
|
|
|
+@Tag(name = "管理端-家庭信息查看", description = "家庭报告/方案/任务概览")
|
|
|
+@RestController
|
|
|
+@RequestMapping("/api/admin")
|
|
|
+public class AdminFamilyOverviewController {
|
|
|
+ @Resource private FamilyAdminOverviewService service;
|
|
|
+
|
|
|
+ @Operation(summary = "家庭信息概览")
|
|
|
+ @PostMapping("/families/{id}/overview")
|
|
|
+ public Result<FamilyOverviewDTO> getOverview(@PathVariable Long id,
|
|
|
+ @RequestAttribute("role") String role) {
|
|
|
+ if (!"admin".equals(role)) return Result.error("无权限");
|
|
|
+ try {
|
|
|
+ return Result.success(service.buildOverview(id));
|
|
|
+ } catch (RuntimeException e) {
|
|
|
+ return Result.error(e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Operation(summary = "家庭任务分页")
|
|
|
+ @PostMapping("/families/{id}/tasks")
|
|
|
+ public Result<Page<TaskItemVO>> getTasks(@PathVariable Long id,
|
|
|
+ @RequestParam(defaultValue = "1") Long page,
|
|
|
+ @RequestParam(defaultValue = "20") Long size,
|
|
|
+ @RequestAttribute("role") String role) {
|
|
|
+ if (!"admin".equals(role)) return Result.error("无权限");
|
|
|
+ return Result.success(service.pageTasks(id, page, size));
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> 注:`Result` 需有 `getCode()`/`getData()`(现有 `Result` 类已有 success/error;若 `getCode()` 不存在用 `r.getCode()` 或对应 getter——以实际 `Result` 类为准)。
|
|
|
+
|
|
|
+- [ ] **Step 4: 运行测试确认通过**
|
|
|
+
|
|
|
+Run: `cd cfc-backend && mvn -q test -Dtest=AdminFamilyOverviewControllerTest -DfailIfNoTests=false`
|
|
|
+Expected: PASS(3/3)
|
|
|
+
|
|
|
+- [ ] **Step 5: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminFamilyOverviewController.java cfc-backend/src/test/java/com/etotem/cfc/integration/controller/AdminFamilyOverviewControllerTest.java
|
|
|
+git commit -m "feat(family): 管理端家庭信息查看接口 overview/tasks"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 6: DatabaseInitializer 存量回填迁移
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`(`runMigrations()` 追加新迁移)
|
|
|
+- Modify: `cfc-backend/src/main/resources/db/schema.sql`(若项目要求同步 schema,见设计文档——本迁移为 DML 回填,无 schema 变更,通常不涉 schema.sql)
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Produces: 幂等迁移 `migrateFamilyNameBackfill()`(DML,`UPDATE families JOIN users`)。
|
|
|
+
|
|
|
+- [ ] **Step 1: 加迁移方法**(在 `runMigrations()` 末尾追加 `migrateFamilyNameBackfill();` 并实现)
|
|
|
+
|
|
|
+```java
|
|
|
+/** 迁移:家庭主账号已填真实姓名但家庭名仍为旧规则 → 回填为「XX家庭」(幂等,重复执行无副作用) */
|
|
|
+private void migrateFamilyNameBackfill() {
|
|
|
+ String sql = "UPDATE families f JOIN users u ON u.id = f.creator_id " +
|
|
|
+ "SET f.name = CONCAT(TRIM(u.real_name), '家庭'), f.updated_at = NOW() " +
|
|
|
+ "WHERE u.real_name IS NOT NULL AND TRIM(u.real_name) <> '' " +
|
|
|
+ "AND f.name <> CONCAT(TRIM(u.real_name), '家庭')";
|
|
|
+ try {
|
|
|
+ int n = jdbcTemplate.update(sql);
|
|
|
+ log.info("迁移[家庭名回填XX家庭] 影响行数={}", n);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移[家庭名回填XX家庭] 执行异常: {}", e.getMessage());
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+(`DatabaseInitializer` 已注入 `JdbcTemplate jdbcTemplate`(第 43-44 行),`jdbcTemplate.update(sql)` 返回受影响行数——沿用现有风格。)
|
|
|
+
|
|
|
+- [ ] **Step 2: 编译 + 确认无回归**
|
|
|
+
|
|
|
+Run: `cd cfc-backend && mvn -q compile -DskipTests`
|
|
|
+Expected: BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **Step 3: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
|
|
|
+git commit -m "feat(family): 存量家庭名回填迁移(幂等)"
|
|
|
+```
|
|
|
+
|
|
|
+> ⚠️ **部署后人工确认点**(非代码步骤):部署重启后,先 dry-run 查影响行数:
|
|
|
+> ```sql
|
|
|
+> SELECT COUNT(*) FROM families f JOIN users u ON u.id=f.creator_id
|
|
|
+> WHERE u.real_name IS NOT NULL AND TRIM(u.real_name)<>'' AND f.name<>CONCAT(TRIM(u.real_name),'家庭');
|
|
|
+> ```
|
|
|
+> 将结果报主理人,确认后再由迁移(启动时自动执行)完成实际回填。**若主理人不批准,跳过**(迁移幂等,可在后续版本再启用)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 7: 前端 API 方法
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-web/src/api/admin.js`(追加两方法)
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Consumes: `request`(`@/utils/request`)
|
|
|
+- Produces: `getFamilyOverview(id)`、`getFamilyTasks(id, params)`。
|
|
|
+
|
|
|
+- [ ] **Step 1: 追加方法**
|
|
|
+
|
|
|
+```js
|
|
|
+export function getFamilyOverview(id) {
|
|
|
+ return request({ url: `/api/admin/families/${id}/overview`, method: 'post' })
|
|
|
+}
|
|
|
+export function getFamilyTasks(id, params) {
|
|
|
+ return request({ url: `/api/admin/families/${id}/tasks`, method: 'post', data: params })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-web/src/api/admin.js
|
|
|
+git commit -m "feat(family): 前端API 家庭信息查看"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 8: FamilyOverviewDialog.vue 组件
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-web/src/views/admin/components/FamilyOverviewDialog.vue`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Consumes: `getFamilyOverview`、`getFamilyTasks`(Task 7);`StatusTag`、`StatCard`、`SectionTitle`。
|
|
|
+- Produces: `<FamilyOverviewDialog :visible.sync="..." :family-id="..." />`,`familyId` 变化时拉取 overview。
|
|
|
+
|
|
|
+- [ ] **Step 1: 实现组件**(结构:基础信息 `el-descriptions` + 3 计数 `StatCard`(`clickable`,点击切 tab)+ `el-tabs` 三个明细表;任务 tab 用 `el-pagination` 服务端分页;枚举映射用 `StatusTag :options`)
|
|
|
+
|
|
|
+模板要点(关键代码,非逐行):
|
|
|
+```html
|
|
|
+<el-dialog :visible.sync="visible" title="家庭信息" width="900px" @open="load">
|
|
|
+ <el-descriptions :column="2" border v-if="overview.family">
|
|
|
+ <el-descriptions-item label="家庭名称">{{ overview.family.name }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="邀请码">{{ overview.family.inviteCode }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="主账号">{{ overview.creatorRealName || overview.creatorName || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="创建时间">{{ formatDate(overview.family.createdAt) }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ <div class="count-cards">
|
|
|
+ <StatCard label="报告" :value="counts.reports" color="primary" clickable @click="activeTab='reports'" />
|
|
|
+ <StatCard label="方案" :value="counts.plans" color="success" clickable @click="activeTab='plans'" />
|
|
|
+ <StatCard label="任务" :value="counts.tasks" color="warning" clickable @click="activeTab='tasks'" />
|
|
|
+ </div>
|
|
|
+ <el-tabs v-model="activeTab">
|
|
|
+ <el-tab-pane label="报告" name="reports">
|
|
|
+ <el-table :data="overview.reports" border max-height="300">
|
|
|
+ <el-table-column label="类型"><template slot-scope="{row}"><StatusTag :options="reportTypeOptions" :value="row.reportType" /></template></el-table-column>
|
|
|
+ <el-table-column prop="reportNumber" label="报告编号" width="140" />
|
|
|
+ <el-table-column label="日期"><template slot-scope="{row}">{{ formatDate(row.reportDate) }}</template></el-table-column>
|
|
|
+ <el-table-column prop="personName" label="姓名" />
|
|
|
+ <el-table-column label="状态"><template slot-scope="{row}"><StatusTag :options="reportStatusOptions" :value="row.status" /></template></el-table-column>
|
|
|
+ <el-table-column prop="overallScore" label="总分" width="70" />
|
|
|
+ <el-table-column prop="gutHealthScore" label="肠道分" width="80" />
|
|
|
+ </el-table>
|
|
|
+ </el-tab-pane>
|
|
|
+ <el-tab-pane label="方案" name="plans">
|
|
|
+ <el-table :data="overview.plans" border max-height="300">
|
|
|
+ <el-table-column label="类型"><template slot-scope="{row}"><StatusTag :options="planTypeOptions" :value="row.planType" /></template></el-table-column>
|
|
|
+ <el-table-column prop="memberName" label="成员" />
|
|
|
+ <el-table-column prop="goal" label="目标" show-overflow-tooltip />
|
|
|
+ <el-table-column label="状态"><template slot-scope="{row}"><StatusTag :options="planStatusOptions" :value="row.status" /></template></el-table-column>
|
|
|
+ <el-table-column prop="totalDays" label="天数" width="70" />
|
|
|
+ <el-table-column label="创建时间"><template slot-scope="{row}">{{ formatDate(row.createdAt) }}</template></el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-tab-pane>
|
|
|
+ <el-tab-pane label="任务" name="tasks">
|
|
|
+ <el-table :data="tasks" border max-height="300">
|
|
|
+ <el-table-column prop="title" label="标题" show-overflow-tooltip />
|
|
|
+ <el-table-column label="状态"><template slot-scope="{row}"><StatusTag :options="taskStatusOptions" :value="row.status" /></template></el-table-column>
|
|
|
+ <el-table-column prop="category" label="分类" width="110" />
|
|
|
+ <el-table-column prop="points" label="积分" width="70" />
|
|
|
+ <el-table-column label="截止"><template slot-scope="{row}">{{ formatDate(row.deadline) }}</template></el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <el-pagination class="pagination-right" @current-change="loadTasks" :current-page="taskPage"
|
|
|
+ :page-size="taskSize" :total="taskTotal" layout="total, prev, pager, next" />
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+</el-dialog>
|
|
|
+```
|
|
|
+
|
|
|
+枚举选项(script data):
|
|
|
+```js
|
|
|
+reportTypeOptions: [
|
|
|
+ { value: 'gut_flora', label: '菌群检测', type: 'success' },
|
|
|
+ { value: 'physical_exam', label: '体检报告', type: 'primary' },
|
|
|
+ { value: 'dan', label: 'DAN测评', type: 'warning' },
|
|
|
+ { value: 'brain_status', label: '脑状态', type: 'primary' },
|
|
|
+ { value: 'aptitude', label: '先天潜能', type: 'primary' },
|
|
|
+ { value: 'insurance', label: '保单', type: 'info' },
|
|
|
+ { value: 'fortune', label: '家庭周报', type: 'danger' }
|
|
|
+],
|
|
|
+reportStatusOptions: [
|
|
|
+ { value: 'active', label: '生效', type: 'success' },
|
|
|
+ { value: 'archived', label: '已归档', type: 'warning' },
|
|
|
+ { value: 'deleted', label: '已删除', type: 'info' }
|
|
|
+],
|
|
|
+planTypeOptions: [{ value: 'health_plan', label: '健康方案', type: 'primary' }, { value: 'family_attribution_plan', label: '家庭归因方案', type: 'warning' }],
|
|
|
+planStatusOptions: [
|
|
|
+ { value: 'draft', label: '草稿', type: 'info' }, { value: 'pending_review', label: '待审核', type: 'warning' },
|
|
|
+ { value: 'published', label: '已发布', type: 'success' }, { value: 'rejected', label: '已驳回', type: 'danger' }
|
|
|
+],
|
|
|
+taskStatusOptions: [
|
|
|
+ { value: 'pending', label: '进行中', type: 'warning' }, { value: 'pending_review', label: '待审核', type: 'warning' },
|
|
|
+ { value: 'completed', label: '已完成', type: 'success' }, { value: 'locked', label: '前置锁定', type: 'info' },
|
|
|
+ { value: 'assigned', label: '待接受', type: 'info' }, { value: 'in_progress', label: '进行中', type: 'warning' },
|
|
|
+ { value: 'rejected', label: '已拒绝', type: 'danger' }, { value: 'overdue', label: '已过期', type: 'danger' },
|
|
|
+ { value: 'cancelled', label: '已取消', type: 'info' }
|
|
|
+]
|
|
|
+```
|
|
|
+
|
|
|
+load / loadTasks:
|
|
|
+```js
|
|
|
+async load() { this.overview = null; const res = await getFamilyOverview(this.familyId); this.overview = res.data; this.loadTasks(1) }
|
|
|
+async loadTasks(page) { this.taskPage = page; const res = await getFamilyTasks(this.familyId, { page, size: this.taskSize }); this.tasks = res.data.records; this.taskTotal = res.data.total }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-web/src/views/admin/components/FamilyOverviewDialog.vue
|
|
|
+git commit -m "feat(family): 家庭信息查看弹窗组件"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 9: Families.vue 接入
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-web/src/views/Families.vue`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Consumes: `FamilyOverviewDialog`(Task 8)
|
|
|
+
|
|
|
+- [ ] **Step 1: 下拉菜单加「查看信息」**(`handleActionCmd` 的 dropdown 内、`viewMembers` 前)
|
|
|
+
|
|
|
+```html
|
|
|
+<el-dropdown-item command="viewOverview" icon="el-icon-view">查看信息</el-dropdown-item>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: script 引入组件与状态**(`import FamilyOverviewDialog from '@/views/admin/components/FamilyOverviewDialog.vue'`;`components:{ FamilyOverviewDialog }`;data 加 `overviewDialogVisible:false, overviewFamilyId:null`)
|
|
|
+
|
|
|
+- [ ] **Step 3: handleActionCmd 加分支 + 方法**
|
|
|
+
|
|
|
+```js
|
|
|
+case 'viewOverview': this.handleViewOverview(row); break;
|
|
|
+// ...
|
|
|
+handleViewOverview(row) { this.overviewFamilyId = row.id; this.overviewDialogVisible = true }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 模板末尾挂组件**
|
|
|
+
|
|
|
+```html
|
|
|
+<FamilyOverviewDialog :visible.sync="overviewDialogVisible" :family-id="overviewFamilyId" />
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: 前端构建验证**
|
|
|
+
|
|
|
+Run: `cd cfc-web && npm run build`
|
|
|
+Expected: BUILD SUCCESS(无编译错误)
|
|
|
+
|
|
|
+- [ ] **Step 6: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-web/src/views/Families.vue
|
|
|
+git commit -m "feat(family): 家庭列表接入查看信息弹窗"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 10: 后端全量测试 + 前端构建(合入前验证)
|
|
|
+
|
|
|
+**Files:** 无新增(验证任务)
|
|
|
+
|
|
|
+- [ ] **Step 1: 后端全量测试(重点相关测试)**
|
|
|
+
|
|
|
+Run: `cd cfc-backend && mvn -q test -Dtest=FamilyNamingSupportTest,UserServiceFamilyNameLinkTest,FamilyAdminOverviewServiceTest,AdminFamilyOverviewControllerTest -DfailIfNoTests=false`
|
|
|
+Expected: ALL PASS
|
|
|
+
|
|
|
+- [ ] **Step 2: 后端整体编译打包**
|
|
|
+
|
|
|
+Run: `cd cfc-backend && mvn clean package -Dmaven.test.skip=true -Dmaven.compile.fork=true`
|
|
|
+Expected: BUILD SUCCESS,产物 `target/cfc-backend-1.0.0.jar`
|
|
|
+
|
|
|
+- [ ] **Step 3: 前端构建**
|
|
|
+
|
|
|
+Run: `cd cfc-web && npm run build`
|
|
|
+Expected: BUILD SUCCESS
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 部署(按 AGENTS.md 流程,人工确认后执行,不属于代码任务)
|
|
|
+
|
|
|
+1. 备份:`tar -czf /app/pro/cfc_backup_$(date +%Y%m%d_%H%M%S)_familyinfo.tar.gz -C /app pro/cfc` + mysqldump `families`/`users` 受影响行。
|
|
|
+2. 后端:`mvn clean package` → `cp target/cfc-backend-1.0.0.jar /app/pro/cfc/backend/` → `systemctl restart cfc-backend`。
|
|
|
+3. 前端:`npm run build` → `rsync -a --delete dist/ /app/pro/cfc/frontend/`。
|
|
|
+4. 验证:`systemctl status cfc-backend`(active)+ `curl POST /api/cart/list` 200 + 新 PID 日志无 ERROR + `/admin/` 200。
|
|
|
+5. **存量回填 dry-run**:执行 Task 6 后的人工确认点 SQL → 报主理人 → 确认后由迁移执行。
|
|
|
+6. 冒烟:管理端家庭列表 → 查看信息弹窗 → 三 tab + 任务翻页;家庭名含真实姓名者显示「XX家庭」。
|