Explorar o código

fix(reports): A方案-报告统一挂 member_id(subject_id 归一化)

- HealthReportService 新增 resolveSubjectToMemberId:users.id / 跨家庭 member id 回查本家庭成员行 id
- bindReportToMember 写库前归一化(集中守卫点);reassignReport 加 familyId==null 守卫
- matchUserByName 三处 return user.getId() 改为 return resolveSubjectToMemberId
- HealthReportController + ReportCollectService buildReportFromPayload 写前归一化
- FamilyMemberService.migrateMemberAssets 按 subject_id=memberId 迁移报告(成员迁移保 id)
- 数据修复: 9 条 health_reports.subject_id 错绑已迁移(mysqldump 备份保留)
Xiaogang Liao hai 1 semana
pai
achega
56149adfe6

+ 2 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java

@@ -2164,7 +2164,8 @@ public class HealthReportController {
         HealthReport report = new HealthReport();
         report.setUserId(draft.getUserId());
         report.setFamilyId(draft.getFamilyId());
-        report.setSubjectId(subjectId);
+        // A方案:subject_id 统一为 family_members.id 口径(users.id 归一化到成员卡)
+        report.setSubjectId(healthReportService.resolveSubjectToMemberId(subjectId, draft.getFamilyId()));
         report.setReportType(draft.getReportType());
         report.setFileUrl(draft.getFileUrl());
         report.setReportFormat(payload.getReportFormat());

+ 11 - 11
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java

@@ -780,18 +780,18 @@ vo.setIsSelf(viewerMember != null && viewerMember.getId().equals(fm.getId()));
         } catch (Exception e) {
             log.warn("迁移能量值异常: memberId={}", memberId, e);
         }
-        // 健康报告(按 userId 或 subjectId 关联)
+        // 健康报告(A方案:按 subject_id=memberId 关联迁移,members.id 不变则报告自动跟随;
+        // 兼容历史 subject_id=users.id 的报告一并迁移,保持 family_id 与成员所在家庭一致)
         try {
-            if (member.getUserId() != null) {
-                List<HealthReport> reports = healthReportMapper.selectList(
-                        new LambdaQueryWrapper<HealthReport>()
-                                .eq(HealthReport::getFamilyId, oldFamilyId)
-                                .and(w -> w.eq(HealthReport::getUserId, member.getUserId())
-                                        .or().eq(HealthReport::getSubjectId, member.getUserId())));
-                for (HealthReport r : reports) {
-                    r.setFamilyId(targetFamilyId);
-                    healthReportMapper.updateById(r);
-                }
+            List<HealthReport> reports = healthReportMapper.selectList(
+                    new LambdaQueryWrapper<HealthReport>()
+                            .eq(HealthReport::getFamilyId, oldFamilyId)
+                            .and(w -> w.eq(HealthReport::getSubjectId, memberId)
+                                    .or(w2 -> w2.isNull(HealthReport::getSubjectId)
+                                            .eq(HealthReport::getUserId, member.getUserId()))));
+            for (HealthReport r : reports) {
+                r.setFamilyId(targetFamilyId);
+                healthReportMapper.updateById(r);
             }
         } catch (Exception e) {
             log.warn("迁移健康报告异常: memberId={}", memberId, e);

+ 69 - 4
cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java

@@ -768,7 +768,7 @@ public class HealthReportService {
             String nm = nameToMatch.trim().replaceAll("\\s+", "");
             if (nm.equals(cleanName)) {
                 log.info("User 姓名精确匹配: {} -> userId={}", cleanName, user.getId());
-                return user.getId();
+                return resolveSubjectToMemberId(user.getId(), familyId);
             }
         }
 
@@ -778,7 +778,7 @@ public class HealthReportService {
 
             if (cleanName.contains(nameToMatch) || nameToMatch.contains(cleanName)) {
                 log.info("User 姓名模糊匹配: {} -> userId={}", cleanName, user.getId());
-                return user.getId();
+                return resolveSubjectToMemberId(user.getId(), familyId);
             }
         }
 
@@ -788,7 +788,7 @@ public class HealthReportService {
 
             if (fuzzyMatch(cleanName, nameToMatch)) {
                 log.info("User 姓名fuzzy匹配: {} -> userId={}", cleanName, user.getId());
-                return user.getId();
+                return resolveSubjectToMemberId(user.getId(), familyId);
             }
         }
 
@@ -1222,9 +1222,71 @@ public class HealthReportService {
                 payloadJson, reportId);
     }
 
+    /**
+     * 归一化 subject_id 到 family_members.id 口径(A方案:报告统一挂 member_id)。
+     *
+     * <p>历史 subject_id 混用 users.id / family_members.id:确认/采集/编辑各链路都可能
+     * 把 users.id 直接写入。本方法把"users.id 或跨家庭成员 id"回查为指定家庭内的
+     * family_members.id,保证报告、维度分、食材推荐全部按成员卡统一挂接,
+     * 成员迁移(改 family_id 保留 member_id)后自动跟随。</p>
+     *
+     * @param subjectId 待归一化的被检测人 id(可能是 users.id 或 family_members.id)
+     * @param familyId  报告所属家庭
+     * @return 该家庭内的 family_members.id;无法解析时原样返回
+     */
+    public Long resolveSubjectToMemberId(Long subjectId, Long familyId) {
+        if (subjectId == null || familyId == null) {
+            return subjectId;
+        }
+        try {
+            // 已是本家庭成员 id -> 原样返回
+            FamilyMember direct = familyMemberMapper.selectById(subjectId);
+            if (direct != null && familyId.equals(direct.getFamilyId())) {
+                return subjectId;
+            }
+            // subjectId 是 users.id -> 回查该用户在本家庭的成员行
+            User u = userMapper.selectById(subjectId);
+            if (u != null) {
+                FamilyMember byUser = familyMemberMapper.selectOne(
+                        new LambdaQueryWrapper<FamilyMember>()
+                                .eq(FamilyMember::getFamilyId, familyId)
+                                .eq(FamilyMember::getUserId, subjectId)
+                                .last("LIMIT 1"));
+                if (byUser != null) {
+                    return byUser.getId();
+                }
+            }
+            // subjectId 是跨家庭 member 行 -> 按该成员的 user_id 回查本家庭
+            if (direct != null && direct.getUserId() != null) {
+                FamilyMember same = familyMemberMapper.selectOne(
+                        new LambdaQueryWrapper<FamilyMember>()
+                                .eq(FamilyMember::getFamilyId, familyId)
+                                .eq(FamilyMember::getUserId, direct.getUserId())
+                                .last("LIMIT 1"));
+                if (same != null) {
+                    return same.getId();
+                }
+            }
+        } catch (Exception e) {
+            log.warn("subjectId归一化失败 subjectId={}, familyId={}: {}", subjectId, familyId, e.getMessage());
+        }
+        return subjectId;
+    }
+
     public void bindReportToMember(Long reportId, Long subjectId) {
         // 使用 JdbcTemplate 直接 UPDATE,避免 MyBatis-Plus NOT_NULL 策略导致 NULL 值不更新
         String sql = "UPDATE health_reports SET subject_id = ? WHERE id = ?";
+        // A方案:写库前归一化为 family_members.id 口径
+        if (subjectId != null && subjectId > 0) {
+            HealthReport rep = healthReportMapper.selectById(reportId);
+            if (rep != null) {
+                Long resolved = resolveSubjectToMemberId(subjectId, rep.getFamilyId());
+                if (resolved != null && !resolved.equals(subjectId)) {
+                    log.info("subjectId归一化 reportId={}, {} -> {}", reportId, subjectId, resolved);
+                }
+                subjectId = resolved;
+            }
+        }
         int updatedRows = jdbcTemplate.update(sql, subjectId, reportId);
         if (updatedRows == 0) {
             log.warn("reportId={}, subjectId={} 更新失败 - 报告不存在", reportId, subjectId);
@@ -1261,7 +1323,10 @@ public class HealthReportService {
             throw new RuntimeException("报告不存在");
         }
         
-        // 权限检查:确保报告属于该家庭
+        // 权限检查:确保报告属于该家庭(familyId 为空视为未加入家庭,防 NPE)
+        if (familyId == null) {
+            throw new RuntimeException("请先加入家庭");
+        }
         if (!familyId.equals(report.getFamilyId())) {
             throw new RuntimeException("报告不属于该家庭");
         }

+ 2 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/ReportCollectService.java

@@ -734,7 +734,8 @@ public class ReportCollectService {
         HealthReport report = new HealthReport();
         report.setUserId(draft.getUserId());
         report.setFamilyId(draft.getFamilyId());
-        report.setSubjectId(subjectId);
+        // A方案:subject_id 统一为 family_members.id 口径(users.id 归一化到成员卡)
+        report.setSubjectId(healthReportService.resolveSubjectToMemberId(subjectId, draft.getFamilyId()));
         report.setReportType(draft.getReportType());
         report.setFileUrl(draft.getFileUrl());
         report.setFileHash(draft.getFileHash());