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

feat(family): 家庭信息查看服务(计数+报告方案全量+任务分页)

Xiaogang Liao 1 неделя назад
Родитель
Сommit
6a53da276e

+ 114 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyAdminOverviewService.java

@@ -0,0 +1,114 @@
+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);
+
+        // 报告明细:全量、按报告日期倒序(同日期按 id 倒序)
+        List<HealthReport> reports = reportMapper.selectList(new LambdaQueryWrapper<HealthReport>()
+                .eq(HealthReport::getFamilyId, familyId)
+                .orderByDesc(HealthReport::getReportDate).orderByDesc(HealthReport::getId));
+        List<FamilyReportItemVO> reportVOs = new ArrayList<>();
+        for (HealthReport r : reports) {
+            FamilyReportItemVO vo = new FamilyReportItemVO();
+            vo.setId(r.getId()); vo.setReportType(r.getReportType()); vo.setReportNumber(r.getReportNumber());
+            vo.setReportDate(r.getReportDate()); vo.setPersonName(r.getPersonName()); vo.setSubjectId(r.getSubjectId());
+            vo.setStatus(r.getStatus()); vo.setOverallScore(r.getOverallScore());
+            vo.setGutHealthScore(r.getGutHealthScore()); vo.setCreatedAt(r.getCreatedAt());
+            reportVOs.add(vo);
+        }
+        dto.setReports(reportVOs);
+
+        // 方案明细:全量、按创建时间倒序(同时间按 id 倒序)
+        List<HealthPlan> plans = planMapper.selectList(new LambdaQueryWrapper<HealthPlan>()
+                .eq(HealthPlan::getFamilyId, familyId)
+                .orderByDesc(HealthPlan::getCreatedAt).orderByDesc(HealthPlan::getId));
+        List<FamilyPlanItemVO> planVOs = new ArrayList<>();
+        for (HealthPlan p : plans) {
+            FamilyPlanItemVO vo = new FamilyPlanItemVO();
+            vo.setId(p.getId()); vo.setPlanType(p.getPlanType()); vo.setMemberName(p.getMemberName());
+            vo.setGoal(p.getGoal()); vo.setStatus(p.getStatus()); vo.setTotalDays(p.getTotalDays());
+            vo.setTeacherId(p.getTeacherId()); vo.setCreatedAt(p.getCreatedAt());
+            planVOs.add(vo);
+        }
+        dto.setPlans(planVOs);
+        return dto;
+    }
+
+    /**
+     * 分组计数:selectMaps + groupBy(MyBatis-Plus 3.5.3.1 的 selectCount 不支持分组,
+     * 分组必须走 selectMaps;COUNT 返回 Long,映射为 Integer)。
+     */
+    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;
+    }
+}

+ 70 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/FamilyAdminOverviewServiceTest.java

@@ -0,0 +1,70 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.dto.FamilyOverviewDTO;
+import com.etotem.cfc.dto.TaskItemVO;
+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 需显式指定 datasource URL,因为 application.yml 的默认值
+ * 指向 192.168.16.251(不可达),而本机 MariaDB(127.0.0.1:3306/zxyj)schema 完整。
+ */
+@SpringBootTest(properties = {
+    "spring.datasource.url=jdbc:mysql://127.0.0.1:3306/zxyj?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true",
+    "spring.datasource.username=zxyj",
+    "spring.datasource.password=zxyj@123"
+})
+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_分页() {
+        // mock 模拟 mapper 收到 page=1,size=20 请求后返回的分页结果(含 size 回显)
+        when(taskMapper.selectPage(any(), any())).thenReturn(new Page<TaskItemVO>() {{
+            setTotal(346); setSize(20); 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; }
+}