Kaynağa Gözat

feat(guide): 新增我的服务家庭与客户家庭成员查询端点

iwt 2 gün önce
ebeveyn
işleme
c163c03e34

+ 49 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideFamilyQueryController.java

@@ -0,0 +1,49 @@
+package com.etotem.cfc.controller.guide;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.service.GuideFamilyAccessGuard;
+import com.etotem.cfc.service.GuideFamilyQueryService;
+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;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "指导师-服务家庭查询", description = "规划师查询自己服务的家庭与客户家庭成员")
+@RestController
+@RequestMapping("/api/guide/families")
+public class GuideFamilyQueryController {
+
+    @Resource
+    private GuideFamilyQueryService guideFamilyQueryService;
+
+    @Resource
+    private GuideFamilyAccessGuard guideFamilyAccessGuard;
+
+    @Operation(summary = "我的服务家庭列表(授权源 guide_families)")
+    @PostMapping("/my-families")
+    public Result<List<Map<String, Object>>> myFamilies(
+            @RequestAttribute("role") String role,
+            @RequestAttribute("userId") Long userId) {
+        // 本端点语义为「我自己的家庭」,无 familyId 维度,故不用 checkFamilyAccess(其要求 familyId 非空)
+        if (!"teacher".equals(role) && !"admin".equals(role)) {
+            return Result.error(403, "无权访问该接口");
+        }
+        return Result.success(guideFamilyQueryService.listMyBoundFamilies(userId));
+    }
+
+    @Operation(summary = "客户家庭成员列表")
+    @PostMapping("/{familyId}/members")
+    public Result<List<FamilyMember>> familyMembers(
+            @RequestAttribute("role") String role,
+            @RequestAttribute("userId") Long userId,
+            @PathVariable("familyId") Long familyId) {
+        Result<Void> denied = guideFamilyAccessGuard.checkFamilyAccess(role, userId, familyId);
+        if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
+
+        return Result.success(guideFamilyQueryService.listFamilyMembers(familyId));
+    }
+}

+ 95 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/GuideFamilyQueryService.java

@@ -0,0 +1,95 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.etotem.cfc.entity.Family;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.entity.GuideFamily;
+import com.etotem.cfc.mapper.FamilyMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.mapper.GuideFamilyMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+@Service
+public class GuideFamilyQueryService {
+
+    @Resource
+    private GuideFamilyMapper guideFamilyMapper;
+
+    @Resource
+    private FamilyMapper familyMapper;
+
+    @Resource
+    private FamilyMemberMapper familyMemberMapper;
+
+    /**
+     * 我的服务家庭:授权源与 isBound() 同为 guide_families(status='binding'),
+     * 保证列表与授权判定同源,避免「列表里有、点进去 403」。
+     */
+    public List<Map<String, Object>> listMyBoundFamilies(Long guideId) {
+        List<GuideFamily> bindings = guideFamilyMapper.selectList(
+                new QueryWrapper<GuideFamily>()
+                        .eq("guide_id", guideId)
+                        .eq("status", "binding")
+                        .orderByDesc("bound_at"));
+        if (bindings == null || bindings.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        List<Long> familyIds = bindings.stream()
+                .map(GuideFamily::getFamilyId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+
+        Map<Long, String> nameByFamily = new HashMap<>();
+        if (!familyIds.isEmpty()) {
+            List<Family> families = familyMapper.selectBatchIds(familyIds);
+            if (families != null) {
+                for (Family f : families) {
+                    nameByFamily.put(f.getId(), f.getName());
+                }
+            }
+        }
+
+        Map<Long, Integer> memberCountByFamily = new LinkedHashMap<>();
+        if (!familyIds.isEmpty()) {
+            List<FamilyMember> members = familyMemberMapper.selectList(
+                    new QueryWrapper<FamilyMember>().in("family_id", familyIds));
+            if (members != null) {
+                for (FamilyMember m : members) {
+                    memberCountByFamily.merge(m.getFamilyId(), 1, Integer::sum);
+                }
+            }
+        }
+
+        List<Map<String, Object>> out = new ArrayList<>();
+        for (GuideFamily gf : bindings) {
+            Long fid = gf.getFamilyId();
+            Map<String, Object> row = new HashMap<>();
+            row.put("familyId", fid);
+            row.put("familyName", nameByFamily.get(fid));
+            row.put("serviceType", gf.getServiceType());
+            row.put("servicePrice", gf.getServicePrice());
+            row.put("boundAt", gf.getBoundAt());
+            row.put("memberCount", memberCountByFamily.getOrDefault(fid, 0));
+            out.add(row);
+        }
+        return out;
+    }
+
+    /** 客户家庭成员列表(显式 familyId,由调用方先做绑定校验) */
+    public List<FamilyMember> listFamilyMembers(Long familyId) {
+        return familyMemberMapper.selectList(
+                new QueryWrapper<FamilyMember>().eq("family_id", familyId));
+    }
+}

+ 111 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/GuideFamilyQueryServiceTest.java

@@ -0,0 +1,111 @@
+package com.etotem.cfc.unit;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.etotem.cfc.entity.Family;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.entity.GuideFamily;
+import com.etotem.cfc.mapper.FamilyMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.mapper.GuideFamilyMapper;
+import com.etotem.cfc.service.GuideFamilyQueryService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+class GuideFamilyQueryServiceTest {
+
+    private GuideFamilyQueryService service;
+    private GuideFamilyMapper gfMapper;
+    private FamilyMapper familyMapper;
+    private FamilyMemberMapper fmMapper;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        service = new GuideFamilyQueryService();
+        gfMapper = mock(GuideFamilyMapper.class);
+        familyMapper = mock(FamilyMapper.class);
+        fmMapper = mock(FamilyMemberMapper.class);
+        set("guideFamilyMapper", gfMapper);
+        set("familyMapper", familyMapper);
+        set("familyMemberMapper", fmMapper);
+    }
+
+    private void set(String n, Object v) throws Exception {
+        Field f = GuideFamilyQueryService.class.getDeclaredField(n);
+        f.setAccessible(true);
+        f.set(service, v);
+    }
+
+    private GuideFamily binding(long familyId) {
+        GuideFamily gf = new GuideFamily();
+        gf.setFamilyId(familyId);
+        gf.setServiceType("paid");
+        gf.setServicePrice(199);
+        return gf;
+    }
+
+    private FamilyMember member(long familyId) {
+        FamilyMember m = new FamilyMember();
+        m.setId(1L);
+        m.setFamilyId(familyId);
+        return m;
+    }
+
+    @Test
+    void 无绑定时返回空列表() {
+        when(gfMapper.selectList(any(QueryWrapper.class))).thenReturn(Collections.emptyList());
+        assertTrue(service.listMyBoundFamilies(1L).isEmpty());
+        verify(familyMapper, never()).selectBatchIds(any());
+    }
+
+    @Test
+    void 返回绑定家庭的名称与成员数() {
+        when(gfMapper.selectList(any(QueryWrapper.class))).thenReturn(Collections.singletonList(binding(10L)));
+
+        Family family = new Family();
+        family.setId(10L);
+        family.setName("张家");
+        when(familyMapper.selectBatchIds(any())).thenReturn(Collections.singletonList(family));
+        when(fmMapper.selectList(any(QueryWrapper.class)))
+                .thenReturn(Arrays.asList(member(10L), member(10L), member(10L)));
+
+        List<Map<String, Object>> out = service.listMyBoundFamilies(1L);
+
+        assertEquals(1, out.size());
+        assertEquals(10L, out.get(0).get("familyId"));
+        assertEquals("张家", out.get(0).get("familyName"));
+        assertEquals(3, out.get(0).get("memberCount"));
+        assertEquals("paid", out.get(0).get("serviceType"));
+    }
+
+    @Test
+    void 多家庭成员数分别统计() {
+        when(gfMapper.selectList(any(QueryWrapper.class)))
+                .thenReturn(Arrays.asList(binding(10L), binding(20L)));
+        when(familyMapper.selectBatchIds(any())).thenReturn(Collections.emptyList());
+        when(fmMapper.selectList(any(QueryWrapper.class)))
+                .thenReturn(Arrays.asList(member(10L), member(10L), member(20L)));
+
+        List<Map<String, Object>> out = service.listMyBoundFamilies(1L);
+
+        assertEquals(2, out.size());
+        assertEquals(2, out.get(0).get("memberCount"));
+        assertEquals(1, out.get(1).get("memberCount"));
+    }
+
+    @Test
+    void 成员列表按familyId查询() {
+        when(fmMapper.selectList(any(QueryWrapper.class))).thenReturn(Collections.emptyList());
+        assertNotNull(service.listFamilyMembers(10L));
+        verify(fmMapper).selectList(any(QueryWrapper.class));
+    }
+}