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

fix(report): 健康报告列表合并展示 DAN 报告(为何之前看不到:DAN 存独立表从未进入 /report/list 数据源,与权限无关)

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

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

@@ -2,6 +2,7 @@ package com.etotem.cfc.controller;
 
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.service.PurchaseService;
+import com.etotem.cfc.service.DanReportUploadService;
 import com.etotem.cfc.dto.HealthAnalysisResult;
 import com.etotem.cfc.dto.MemberMatchResult;
 import com.etotem.cfc.dto.ParsedDiseaseRisk;
@@ -20,6 +21,7 @@ import com.etotem.cfc.entity.HealthGutFlora;
 import com.etotem.cfc.entity.HealthIndicator;
 import com.etotem.cfc.entity.HealthReport;
 import com.etotem.cfc.entity.HealthReportDraft;
+import com.etotem.cfc.entity.DanReportUpload;
 import com.etotem.cfc.entity.ReportTransferRequest;
 import com.etotem.cfc.entity.NutritionDeficiencyRecord;
 import com.etotem.cfc.entity.NutritionIndicatorMapping;
@@ -143,6 +145,9 @@ public class HealthReportController {
     @Resource
     private ReportMemberPermissionService reportMemberPermissionService;
 
+    @Resource
+    private DanReportUploadService danReportUploadService;
+
     @Resource
     private FoodRecommendService foodRecommendService;
 
@@ -317,6 +322,7 @@ public class HealthReportController {
         int size = params.get("size") != null ? Integer.parseInt(params.get("size").toString()) : 0;
         Long memberId = ParamUtils.getLong(params.get("memberId"));
         List<HealthReport> reports;
+        java.util.Set<Long> visibleForDan = null;
         if (memberId != null) {
             // 成员级权限:管理员 / 本人 / 被授权成员可见,否则返回空列表
             if (familyId == null || !reportMemberPermissionService.canViewMemberReports(userId, memberId, familyId)) {
@@ -340,15 +346,75 @@ public class HealthReportController {
                         reports.add(r);
                     }
                 }
+                // DAN 报告按可见成员过滤(family_members.id 语义)
+                visibleForDan = visible;
             }
         } else {
             // 无家庭信息时返回空列表,避免前端报错
             return Result.success(new ArrayList<>());
         }
+        // 合并 DAN 报告(已确认入库的)到列表,保证报告列表页能看到 DAN 报告
+        List<HealthReport> merged = mergeDanReports(reports, memberId, familyId, visibleForDan, userId);
+
         if (size > 0) {
-            return Result.success(limitLatest(reports, size));
+            return Result.success(limitLatest(merged, size));
         }
-        return Result.success(reports);
+        return Result.success(merged);
+    }
+
+    /**
+     * 合并 DAN 报告到健康报告列表(转成 HealthReport 兼容结构,reportType='dan')
+     */
+    private List<HealthReport> mergeDanReports(List<HealthReport> reports, Long memberId, Long familyId,
+                                               java.util.Set<Long> visibleForDan, Long userId) {
+        List<DanReportUpload> danList = danReportUploadService.listConfirmedForReportList(memberId, familyId, visibleForDan);
+        if (danList.isEmpty()) {
+            return reports == null ? new ArrayList<>() : reports;
+        }
+        java.util.Map<Long, Long> idToMemberId = new java.util.HashMap<>();
+        for (DanReportUpload u : danList) {
+            if (u.getFamilyMemberId() != null) {
+                idToMemberId.put(u.getId(), u.getFamilyMemberId());
+            }
+        }
+        List<HealthReport> result = reports == null ? new ArrayList<>() : new ArrayList<>(reports);
+        // 成员昵称缓存(personName 展示)
+        java.util.Map<Long, String> memberNameCache = new java.util.HashMap<>();
+        for (DanReportUpload u : danList) {
+            HealthReport hr = new HealthReport();
+            hr.setId(u.getId());
+            hr.setReportType("dan");
+            hr.setFamilyId(u.getFamilyId());
+            hr.setSubjectId(u.getFamilyMemberId());
+            hr.setUserId(u.getUploaderUserId());
+            hr.setCreatedAt(u.getUploadedAt() != null ? u.getUploadedAt() : u.getConfirmedAt());
+            hr.setReportDate(u.getConfirmedAt() != null ? u.getConfirmedAt() : u.getUploadedAt());
+            if (u.getFamilyMemberId() != null) {
+                String name = memberNameCache.get(u.getFamilyMemberId());
+                if (name == null) {
+                    try {
+                        com.etotem.cfc.entity.FamilyMember fm = familyMemberMapper.selectById(u.getFamilyMemberId());
+                        name = fm != null ? (fm.getNickname() != null ? fm.getNickname() : ("成员" + fm.getId())) : null;
+                    } catch (Exception e) {
+                        name = null;
+                    }
+                    if (name == null) name = "";
+                    memberNameCache.put(u.getFamilyMemberId(), name);
+                }
+                hr.setPersonName(name);
+            }
+            hr.setStatus("active");
+            result.add(hr);
+        }
+        // 按上传时间倒序(DAN 与健康报告统一排序)
+        result.sort((a, b) -> {
+            java.util.Date da = a.getCreatedAt();
+            java.util.Date db = b.getCreatedAt();
+            if (da == null) return 1;
+            if (db == null) return -1;
+            return db.compareTo(da);
+        });
+        return result;
     }
 
     /**

+ 27 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DanReportUploadService.java

@@ -558,6 +558,33 @@ if (!DanReportUpload.DIM_MIND.equals(dimension) && !DanReportUpload.DIM_WISDOM.e
         return Result.success(result);
     }
 
+    /**
+     * 报告列表页合并用:查询已确认的DAN报告(confirmed 且未删除)
+     *
+     * @param memberId         指定家庭成员(family_members.id),非空时按成员过滤
+     * @param familyId         家庭ID(memberId 为空时按家庭过滤)
+     * @param visibleMemberIds 可见成员集合(普通成员的可见身份 family_members.id;管理员传 null 表示全部)
+     */
+    public java.util.List<DanReportUpload> listConfirmedForReportList(Long memberId, Long familyId, java.util.Set<Long> visibleMemberIds) {
+        LambdaQueryWrapper<DanReportUpload> q = new LambdaQueryWrapper<>();
+        if (memberId != null) {
+            q.eq(DanReportUpload::getFamilyMemberId, memberId);
+        } else if (familyId != null) {
+            q.eq(DanReportUpload::getFamilyId, familyId);
+        } else {
+            return new java.util.ArrayList<>();
+        }
+        q.eq(DanReportUpload::getDraftStatus, DanReportUpload.DRAFT_CONFIRMED)
+                .isNull(DanReportUpload::getDeletedAt);
+        // 普通成员:仅按可见成员过滤(管理员可见集合为 null 表示全部)
+        if (visibleMemberIds != null && memberId == null) {
+            q.in(DanReportUpload::getFamilyMemberId, visibleMemberIds);
+        }
+        q.orderByDesc(DanReportUpload::getUploadedAt);
+        SortUtil.applySort(q);
+        return uploadMapper.selectList(q);
+    }
+
     /**
      * 供控制器做权限校验用:仅取记录本体(不做 blocks 拼装),避免重复查库
      * 返回 null 表示记录不存在(控制器需 fallback 调 getDetail 以取得错误信息)

+ 17 - 6
cfc-frontend/pages/health/report-list.vue

@@ -66,9 +66,9 @@
 
     <!-- 报告列表 -->
     <view class="report-list" v-else-if="reports.length > 0">
-      <view class="swipe-row" v-for="item in reports" :key="item.id">
-        <!-- 右侧操作按钮(左滑后露出:删除 + 调整归属) -->
-        <view class="swipe-actions">
+      <view class="swipe-row" v-for="item in reports" :key="item.reportType + '-' + item.id">
+        <!-- 右侧操作按钮(左滑后露出:删除 + 调整归属);DAN 报告不可左右滑动删除 -->
+        <view class="swipe-actions" v-if="item.reportType !== 'dan'">
           <view class="swipe-delete" @click.stop="confirmDelete(item)">删除</view>
           <view class="swipe-adjust" @click.stop="adjustBinding(item)">调整归属</view>
         </view>
@@ -82,8 +82,8 @@
               @click="onRowClick(item)">
           <view class="report-card">
             <view class="report-card-left">
-              <view class="report-type-badge" :class="'badge-' + (item.reportType === 'gut_flora' ? 'gut' : item.reportType === 'tongue' ? 'tongue' : 'exam')">
-                <text>{{ item.reportType === 'gut_flora' ? '肠道检测' : item.reportType === 'tongue' ? '舌诊' : '体检报告' }}</text>
+              <view class="report-type-badge" :class="'badge-' + (item.reportType === 'gut_flora' ? 'gut' : item.reportType === 'tongue' ? 'tongue' : item.reportType === 'dan' ? 'dan' : 'exam')">
+                <text>{{ item.reportType === 'gut_flora' ? '肠道检测' : item.reportType === 'tongue' ? '舌诊' : item.reportType === 'dan' ? 'DAN测评' : '体检报告' }}</text>
               </view>
               <text class="report-person" v-if="item.personName">{{ item.personName }}</text>
               <text class="report-date">报告日期:{{ formatDate(item.reportDate) }}</text>
@@ -102,7 +102,7 @@
     <view class="empty-wrap" v-else>
       <text class="empty-icon">📋</text>
       <text class="empty-title">暂无健康报告</text>
-      <text class="empty-desc">上传体检报告或肠道检测报告,获取 解读</text>
+      <text class="empty-desc">上传体检报告、肠道检测或 DAN 测评报告,获取 解读</text>
       <view class="empty-btn" @click="goToUpload">上传报告</view>
     </view>
 
@@ -479,6 +479,12 @@ export default {
       this.goToDetail(item.id, item.reportType)
     },
     onTouchStart(e, item) {
+      // DAN 报告无删除/调整归属能力,禁止左滑
+      if (item && item.reportType === 'dan') {
+        this.curId = null
+        return
+      }
+
       // 先收起其它已打开行
       if (this.curId !== null && this.curId !== item.id && this.openMap[this.curId]) {
         this.$set(this.openMap, this.curId, false)
@@ -816,6 +822,11 @@ export default {
   border-radius: 20rpx;
   align-self: flex-start;
 }
+.badge-dan {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #FFFFFF;
+}
+
 .badge-exam {
   background: #E3F2FD;
   color: #1565C0;