|
|
@@ -1,6 +1,7 @@
|
|
|
package com.etotem.cfc.service;
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
+import com.etotem.cfc.common.DataMaskUtil;
|
|
|
import com.etotem.cfc.entity.Family;
|
|
|
import com.etotem.cfc.entity.FamilyMember;
|
|
|
import com.etotem.cfc.entity.GuideFamily;
|
|
|
@@ -92,4 +93,112 @@ public class GuideFamilyQueryService {
|
|
|
return familyMemberMapper.selectList(
|
|
|
new QueryWrapper<FamilyMember>().eq("family_id", familyId));
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 客户档案导出(合规,sc-key-map SC-T-29 → 能力键 {@code teacher.client.export}):
|
|
|
+ * 导出规划师名下绑定家庭({@code guide_families.status='binding'},与 {@code isBound()} 同源授权)的客户档案。
|
|
|
+ *
|
|
|
+ * <p><b>导出范围显式入参</b>:{@code familyId} 非空 → 仅导出该绑定家庭的客户(越授权拦截由调用方守卫
|
|
|
+ * {@code GuideFamilyAccessGuard#checkBinding} 负责);为 null → 导出全部绑定客户。</p>
|
|
|
+ *
|
|
|
+ * <p><b>合规口径</b>:敏感字段 {@code phone/idCard} 强制脱敏({@link DataMaskUtil},对齐 SC-S-11
|
|
|
+ * {@code supplier.data.exportCompliance} 契约口径,个保法最小化)。</p>
|
|
|
+ *
|
|
|
+ * <p><b>历史约束</b>:不依赖 {@code children} 表全字段快照(生产缺 {@code phone/id_card} 两列,
|
|
|
+ * 见根 AGENTS.md「children 表标注冲突」),统一经 {@code family_members} 聚合导出。</p>
|
|
|
+ *
|
|
|
+ * @param guideId 规划师(teacher)userId
|
|
|
+ * @param familyId 可选导出范围(家庭ID);null 导出全部绑定客户
|
|
|
+ * @return 导出包:{@code {families:[{familyId, familyName, serviceType, boundAt, clients:[{memberId, nickname,
|
|
|
+ * phone(脱敏), idCard(脱敏), gender, age, birthday, memberType}]}]}};无绑定客户时返回空 Map
|
|
|
+ */
|
|
|
+ public Map<String, Object> exportClientArchive(Long guideId, Long familyId) {
|
|
|
+ Map<String, Object> out = new HashMap<>();
|
|
|
+ List<GuideFamily> bindings;
|
|
|
+ if (familyId != null) {
|
|
|
+ bindings = guideFamilyMapper.selectList(
|
|
|
+ new QueryWrapper<GuideFamily>()
|
|
|
+ .eq("guide_id", guideId)
|
|
|
+ .eq("family_id", familyId)
|
|
|
+ .eq("status", "binding"));
|
|
|
+ } else {
|
|
|
+ bindings = guideFamilyMapper.selectList(
|
|
|
+ new QueryWrapper<GuideFamily>()
|
|
|
+ .eq("guide_id", guideId)
|
|
|
+ .eq("status", "binding")
|
|
|
+ .orderByDesc("bound_at"));
|
|
|
+ }
|
|
|
+ if (bindings == null || bindings.isEmpty()) {
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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) {
|
|
|
+ if (f != null && f.getId() != null) {
|
|
|
+ nameByFamily.put(f.getId(), f.getName());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<Long, List<FamilyMember>> membersByFamily = new HashMap<>();
|
|
|
+ if (!familyIds.isEmpty()) {
|
|
|
+ List<FamilyMember> members = familyMemberMapper.selectList(
|
|
|
+ new QueryWrapper<FamilyMember>().in("family_id", familyIds));
|
|
|
+ if (members != null) {
|
|
|
+ for (FamilyMember m : members) {
|
|
|
+ if (m == null || m.getFamilyId() == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ membersByFamily.computeIfAbsent(m.getFamilyId(), k -> new ArrayList<>()).add(m);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ List<Map<String, Object>> families = new ArrayList<>();
|
|
|
+ for (GuideFamily gf : bindings) {
|
|
|
+ Long fid = gf.getFamilyId();
|
|
|
+ if (fid == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Map<String, Object> familyRow = new HashMap<>();
|
|
|
+ familyRow.put("familyId", fid);
|
|
|
+ familyRow.put("familyName", nameByFamily.get(fid));
|
|
|
+ familyRow.put("serviceType", gf.getServiceType());
|
|
|
+ familyRow.put("boundAt", gf.getBoundAt());
|
|
|
+ List<Map<String, Object>> clients = new ArrayList<>();
|
|
|
+ List<FamilyMember> mbrs = membersByFamily.getOrDefault(fid, Collections.emptyList());
|
|
|
+ for (FamilyMember m : mbrs) {
|
|
|
+ clients.add(toMaskedClient(m));
|
|
|
+ }
|
|
|
+ familyRow.put("clients", clients);
|
|
|
+ families.add(familyRow);
|
|
|
+ }
|
|
|
+ out.put("families", families);
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 客户档案单行导出载体:敏感字段(phone/idCard)强制脱敏。 */
|
|
|
+ private Map<String, Object> toMaskedClient(FamilyMember m) {
|
|
|
+ Map<String, Object> client = new HashMap<>();
|
|
|
+ client.put("memberId", m.getId());
|
|
|
+ client.put("nickname", m.getNickname());
|
|
|
+ client.put("gender", m.getGender());
|
|
|
+ client.put("age", m.getAge());
|
|
|
+ client.put("birthday", m.getBirthday());
|
|
|
+ client.put("memberType", m.getMemberType());
|
|
|
+ // 合规:个保法最小化,对齐 SC-S-11 supplier.data.exportCompliance 契约口径
|
|
|
+ client.put("phone", DataMaskUtil.maskPhone(m.getPhone()));
|
|
|
+ client.put("idCard", DataMaskUtil.maskIdCard(m.getIdCard()));
|
|
|
+ return client;
|
|
|
+ }
|
|
|
}
|