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

refactor(member): T25 P2 — 报告归属/互动/DOA/能量/上传页统一 memberId 口径 + 折叠同用户重复行

- report-list: 顶部成员过滤改用 MemberSelector(minimal, showOverview=全部),
  去掉 :class 内联方法调用,改为预计算 _filterId=memberIdOf
- gut-flora-detail / report-detail / report-confirm: 成员选择器与候选列表
  预计算 _mid 供 :class 内联比较,loadFamilyMembers 走 normalizeMembers,
  selectAndClose/createMember setter 统一 memberIdOf
- interaction-log: loadMembers 走 normalizeMembers + findSelfMember/membersExceptSelf,
  :key 与 selectedMemberId 改用 canonical memberId/selectorKey
- doa-stage-create: members 折叠后按 canonical memberId 比对与提交
- report-upload: resolveMemberId 委托 memberIdOf,familyMembers 折叠
- energy/index: 家族成员列表折叠防御层
- MemberChipPicker: 多选语义不变,仅补折叠防御层(消除同一用户双 chip)
- activity-detail: 报名人 memberId 去掉 userId 兜底,与 'c_'+id 前缀口径一致
Sisyphus 2 недель назад
Родитель
Сommit
d9c796941e

+ 9 - 17
cfc-frontend/pages/action-detail/interaction-log.vue

@@ -9,8 +9,8 @@
     <view class="form-section" v-if="!preselectedMemberId">
       <text class="section-label">互动对象</text>
       <view class="member-select">
-        <view class="member-item" v-for="m in members" :key="m.id"
-              :class="{selected: selectedMemberId === m.id}"
+        <view class="member-item" v-for="m in members" :key="m.selectorKey"
+              :class="{selected: selectedMemberId === m.memberId}"
               @click="selectMember(m)">
           <text class="member-name">{{ m.nickname }}</text>
           <text class="member-rel">{{ m.relationship || m.relativeLabel || m.roleLabel }}</text>
@@ -125,6 +125,7 @@
 </template>
 
 <script>
+import { normalizeMembers, memberIdOf, findSelfMember, membersExceptSelf } from '../../utils/memberNormalize.js'
 import { getFamilyMemberList, addInteraction, reportAppEnter
 } from '../../utils/api.js'
 import config from '../../config.js'
@@ -193,28 +194,19 @@ export default {
       try {
         var res = await getFamilyMemberList()
         if (res.code === 200 && res.data) {
-          var all = res.data || []
-          // 使用后端返回的 isSelf 标记(比匹配 userId 更可靠)
-          var found = null
-          for (var i = 0; i < all.length; i++) {
-            var m = all[i]
-            if (m && m.isSelf === true) {
-              found = m
-              break
-            }
-          }
-          this.currentMemberId = found ? found.id : null
+          // 前端防御层:按 user_id 折叠同用户多行,isSelf 唯一;统一身份键 memberId
+          var list = normalizeMembers(res.data || [], { userId: uni.getStorageSync('userId') })
+          var selfRow = findSelfMember(list)
+          this.currentMemberId = selfRow ? memberIdOf(selfRow) : null
           // 互动对象列表过滤掉"我"(from==to 后端会拒绝)
-          this.members = all.filter(function(m) {
-            return !(m && m.isSelf === true)
-          })
+          this.members = membersExceptSelf(list)
         }
       } catch (e) {
         uni.showToast({ title: '加载成员失败', icon: 'none' })
       }
     },
     selectMember(member) {
-      this.selectedMemberId = member.id
+      this.selectedMemberId = memberIdOf(member)
     },
     selectType(type) {
       this.selectedType = type

+ 1 - 1
cfc-frontend/pages/activity/activity-detail/activity-detail.vue

@@ -731,7 +731,7 @@ export default {
                 name: m.nickname || m.name || '',
                 phone: m.phone || '',
                 relation: '孩子',
-                memberId: m.userId || m.id,
+                memberId: m.id,
                 source: 'child',
                 targetType: 'member'
               })

+ 3 - 1
cfc-frontend/pages/energy/index.vue

@@ -142,6 +142,7 @@
 <script>
 import { primary, success } from '@/utils/brand-colors.js'
 import RadarChart from '../../components/RadarChart.vue'
+import { normalizeMembers } from '../../utils/memberNormalize.js'
 import { getFamilyEnergySandbox, getFamilyMemberList } from '../../utils/api.js'
 import { BrandColors } from '../../utils/brand-colors.js'
 
@@ -324,7 +325,8 @@ export default {
         // 加载家庭成员(visibleOnly 列表,用于 isSelf/name 等展示属性)
         var memRes = await getFamilyMemberList({ visibleOnly: true })
         if (memRes && memRes.code === 200 && memRes.data) {
-          this.familyMembers = memRes.data
+          // 前端防御层:按 user_id 折叠同用户多行,isSelf 唯一;统一身份键 memberId
+          this.familyMembers = normalizeMembers(memRes.data, { userId: uni.getStorageSync('userId') })
         }
         // 确定默认选中成员:优先全局切换的成员
         var switchedId = uni.getStorageSync('currentMemberId') || uni.getStorageSync('currentChildId')

+ 4 - 1
cfc-frontend/pages/health/components/MemberChipPicker.vue

@@ -26,6 +26,7 @@
 
 <script>
 import { getFamilyMemberList } from '@/utils/api.js'
+import { normalizeMembers } from '@/utils/memberNormalize.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
 
 export default {
@@ -175,7 +176,9 @@ export default {
           })
         })
       }).then(function(list) {
-        var count = self.applyList(list || [])
+        // 前端防御层:按 user_id 折叠同用户多行(消除"本人 + 本人所对应的成员"双 chip)
+        var normalized = normalizeMembers(list || [], { userId: uni.getStorageSync('userId') })
+        var count = self.applyList(normalized)
         if (count === 0) {
           uni.showToast({ title: '成员列表为空', icon: 'none' })
         }

+ 6 - 4
cfc-frontend/pages/health/doa-stage-create.vue

@@ -6,9 +6,9 @@
       <view class="member-grid">
         <view
           v-for="m in members"
-          :key="m.id"
+          :key="m.selectorKey"
           class="member-item"
-          :class="{ active: form.memberId === m.id }"
+          :class="{ active: form.memberId === m.memberId }"
           @click="selectMember(m)"
         >
           <text>{{ m.nickname }}</text>
@@ -60,6 +60,7 @@
 </template>
 
 <script>
+import { normalizeMembers, memberIdOf } from '../../utils/memberNormalize.js'
 import { createDoaStage, getFamilyMemberList } from '../../utils/api.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
 
@@ -107,7 +108,8 @@ export default {
     async loadMembers() {
       try {
         var res = await getFamilyMemberList({ visibleOnly: true })
-        var list = res.data || []
+        // 前端防御层:按 user_id 折叠同用户多行,isSelf 唯一;统一身份键 memberId
+        var list = normalizeMembers(res.data || [], { userId: uni.getStorageSync('userId') })
         this.members = list
         this.hasLocalMembers = list.some(function(m) { return !m.userId })
         // 若从总览"为他创建"进入且目标成员未登录 → 直接锁定
@@ -120,7 +122,7 @@ export default {
       }
     },
     selectMember(m) {
-      this.form.memberId = m.id
+      this.form.memberId = memberIdOf(m)
     },
     addGoal() {
       if (this.form.goals.length >= 3) return

+ 11 - 6
cfc-frontend/pages/health/gut-flora-detail.vue

@@ -276,7 +276,7 @@
             class="candidate-item"
             v-for="(cand, idx) in candidates"
             :key="idx"
-            :class="{ 'candidate-selected': selectedMemberId === (cand.id || cand.userId) }"
+            :class="{ 'candidate-selected': selectedMemberId === cand._mid }"
             @tap="selectAndCloseDraft(cand)"
           >
             <view class="candidate-left">
@@ -295,7 +295,7 @@
             class="member-item"
             v-for="(member, mIdx) in familyMembers"
             :key="mIdx"
-            :class="{ 'member-selected': selectedMemberId === (member.userId || member.id) }"
+            :class="{ 'member-selected': selectedMemberId === member._mid }"
             @tap="selectAndCloseDraft(member)"
           >
             <text class="member-avatar">{{ (member.nickname || '?').substring(0, 1) }}</text>
@@ -354,6 +354,7 @@
 </template>
 
 <script>
+import { normalizeMembers, memberIdOf } from '../../utils/memberNormalize.js'
 import { getReportDetail, editHealthReport, queryKnowledgeBase, queryIndicatorKnowledge, getGutFloraAnalysis, confirmReportPreview, discardReportDraft, parseReportDraft, getHealthFoods, getFoodCautionList, getFamilyMemberList, addFamilyMember, getIndicatorChangeRanking } from '../../utils/api.js'
 import HealthKnowledgePopup from '@/pages/health/components/health-knowledge-popup.vue'
 import { BrandColors } from '../../utils/brand-colors.js'
@@ -552,12 +553,15 @@ riskGroupAbnormalCount: function() {
       var self = this
       getFamilyMemberList({ visibleOnly: true }).then(function(res) {
         if (res.code === 200) {
-          self.familyMembers = res.data || []
+          // 前端防御层:按 user_id 折叠同用户多行,isSelf 唯一;统一身份键 memberId
+          var list = normalizeMembers(res.data || [], { userId: uni.getStorageSync('userId') })
+          for (var i = 0; i < list.length; i++) list[i]._mid = memberIdOf(list[i])
+          self.familyMembers = list
         }
       }).catch(function() {})
     },
     selectAndCloseDraft: function(member) {
-      this.selectedMemberId = member.id || member.userId
+      this.selectedMemberId = member._mid != null ? member._mid : memberIdOf(member)
       this.selectedMemberObj = member
       this.createdMemberId = null
       this.needBind = false
@@ -590,7 +594,7 @@ riskGroupAbnormalCount: function() {
         self.creatingMember = false
         if (res.code === 200) {
           var member = res.data || {}
-          self.selectedMemberId = member.id || member.userId
+          self.selectedMemberId = memberIdOf(member)
           self.selectedMemberObj = member
           self.createdMemberId = self.selectedMemberId
           self.needBind = false
@@ -699,12 +703,13 @@ riskGroupAbnormalCount: function() {
         self.matchConfidence = res.data.confidence !== undefined ? parseFloat(res.data.confidence) : null
         self.matchLabel = res.data.matchLabel || ''
         self.candidates = res.data.candidates || []
+        for (var ci = 0; ci < self.candidates.length; ci++) self.candidates[ci]._mid = memberIdOf(self.candidates[ci])
         // 高置信度自动选中
         if (self.matchedMemberId && self.matchConfidence >= 0.8) {
           self.selectedMemberId = self.matchedMemberId
           // 在 candidates 中找对应对象
           for (var i = 0; i < self.candidates.length; i++) {
-            if ((self.candidates[i].id || self.candidates[i].userId) === self.selectedMemberId) {
+            if (self.candidates[i]._mid === self.selectedMemberId) {
               self.selectedMemberObj = self.candidates[i]
               break
             }

+ 16 - 11
cfc-frontend/pages/health/report-confirm.vue

@@ -43,7 +43,7 @@
               class="candidate-item"
               v-for="(cand, idx) in candidates"
               :key="idx"
-              :class="{ 'candidate-selected': selectedMemberId === (cand.id || cand.userId) }"
+              :class="{ 'candidate-selected': selectedMemberId === cand._mid }"
               @click="selectAndClose(cand)"
             >
               <view class="candidate-left">
@@ -70,7 +70,7 @@
               class="member-item"
               v-for="(member, mIdx) in otherMembers"
               :key="mIdx"
-              :class="{ 'member-selected': selectedMemberId === (member.userId || member.id) }"
+              :class="{ 'member-selected': selectedMemberId === member._mid }"
               @click="selectAndClose(member)"
             >
               <text class="member-avatar">{{ (member.nickname || '?').substring(0, 1) }}</text>
@@ -356,6 +356,7 @@
 </template>
 
 <script>
+import { normalizeMembers, memberIdOf } from '../../utils/memberNormalize.js'
 import { confirmReportPreview, discardReportDraft, confirmTongue, discardTongue, parseReportDraft } from '../../utils/api.js'
 import { addFamilyMember, getFamilyMemberList } from '../../utils/api.js'
 import FeedbackPopup from '@/pages/health/components/feedback-popup.vue'
@@ -461,14 +462,13 @@ export default {
     otherMembers() {
       var candIds = {}
       for (var i = 0; i < this.candidates.length; i++) {
-        var c = this.candidates[i]
-        var cid = c.id || c.userId
+        var cid = this.candidates[i]._mid
         if (cid !== null && cid !== undefined) candIds[cid] = true
       }
       var list = []
       for (var j = 0; j < this.familyMembers.length; j++) {
         var m = this.familyMembers[j]
-        var mid = m.userId || m.id
+        var mid = m._mid
         if (mid !== null && mid !== undefined && candIds[mid]) continue
         list.push(m)
       }
@@ -575,6 +575,7 @@ export default {
     if (candidatesStr) {
       try {
         this.candidates = JSON.parse(candidatesStr)
+        for (var ci = 0; ci < this.candidates.length; ci++) this.candidates[ci]._mid = memberIdOf(this.candidates[ci])
       } catch (e) {
         this.candidates = []
       }
@@ -644,6 +645,7 @@ export default {
       this.matchConfidence = detail.confidence !== undefined && detail.confidence !== null ? parseFloat(detail.confidence) : null
       this.matchLabel = detail.matchLabel || ''
       this.candidates = detail.candidates || []
+      for (var ci = 0; ci < this.candidates.length; ci++) this.candidates[ci]._mid = memberIdOf(this.candidates[ci])
 
       // 始终加载家庭成员
       this.loadFamilyMembers()
@@ -659,12 +661,14 @@ export default {
       getFamilyMemberList({ visibleOnly: true }).then(function(res) {
         var list = (res && res.data) || []
         var parents = list.filter(function(m) { return m.source === 'parent' }).map(function(p) {
-          return { id: p.id, nickname: p.nickname, gender: p.gender, role: 'parent' }
+          return { id: p.id, userId: p.userId, nickname: p.nickname, gender: p.gender, role: 'parent' }
         })
         var children = list.filter(function(m) { return m.source !== 'parent' }).map(function(c) {
-          return { id: c.id, nickname: c.nickname, gender: c.gender, age: c.age, role: 'child' }
+          return { id: c.id, userId: c.userId, nickname: c.nickname, gender: c.gender, age: c.age, role: 'child' }
         })
-        self.familyMembers = parents.concat(children)
+        // 前端防御层:按 user_id 折叠同用户多行,isSelf 唯一;统一身份键 memberId
+        self.familyMembers = normalizeMembers(parents.concat(children), { userId: uni.getStorageSync('userId') })
+        for (var i = 0; i < self.familyMembers.length; i++) self.familyMembers[i]._mid = memberIdOf(self.familyMembers[i])
         self.syncSelectedMemberObj()
       }).catch(function(e) {
         console.log('加载家庭成员失败', e)
@@ -678,7 +682,7 @@ export default {
       }
     },
     selectAndClose(member) {
-      var memberId = member.id || member.userId
+      var memberId = member._mid != null ? member._mid : memberIdOf(member)
       this.selectedMemberId = memberId
       this.selectedMemberObj = member
       this.extractedName = member.nickname || member.name || this.extractedName
@@ -695,7 +699,8 @@ export default {
       var all = this.familyMembers.concat(this.candidates)
       for (var i = 0; i < all.length; i++) {
         var m = all[i]
-        if ((m.id || m.userId) === this.selectedMemberId) {
+        var mid = m._mid != null ? m._mid : memberIdOf(m)
+        if (mid === this.selectedMemberId) {
           this.selectedMemberObj = m
           return
         }
@@ -732,7 +737,7 @@ export default {
       }).then(function(res) {
         uni.hideLoading()
         if (res.code === 200) {
-          var memberId = res.data && (res.data.userId || res.data.id)
+          var memberId = res.data && memberIdOf(res.data)
           if (memberId) {
             self.createdMemberId = memberId
             self.selectedMemberId = memberId

+ 14 - 10
cfc-frontend/pages/health/report-detail.vue

@@ -67,7 +67,7 @@
                   class="candidate-item"
                   v-for="(cand, idx) in candidates"
                   :key="idx"
-                  :class="{ 'candidate-selected': selectedMemberId === (cand.id || cand.userId) }"
+                  :class="{ 'candidate-selected': selectedMemberId === cand._mid }"
                   @click="selectAndClose(cand)"
                 >
                   <view class="candidate-left">
@@ -94,7 +94,7 @@
                   class="member-item"
                   v-for="(member, mIdx) in otherMembers"
                   :key="mIdx"
-                  :class="{ 'member-selected': selectedMemberId === (member.userId || member.id) }"
+                  :class="{ 'member-selected': selectedMemberId === member._mid }"
                   @click="selectAndClose(member)"
                 >
                   <text class="member-avatar">{{ (member.nickname || '?').substring(0, 1) }}</text>
@@ -407,6 +407,7 @@
 <script>
 import ReportBlocksRenderer from './report-blocks-renderer.vue'
 import FeedbackPopup from '@/pages/health/components/feedback-popup.vue'
+import { normalizeMembers, memberIdOf } from '../../utils/memberNormalize.js'
 import { getReportDetail, getDanReportDetail, getReportPayload, editHealthReport, parseReportDraft, confirmReportPreview, discardReportDraft, confirmTongue, discardTongue, getFamilyMemberList, addFamilyMember, queryIndicatorKnowledge, queryBacteriaKnowledge } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
@@ -519,14 +520,13 @@ export default {
     otherMembers() {
       var candIds = {}
       for (var i = 0; i < this.candidates.length; i++) {
-        var c = this.candidates[i]
-        var cid = c.id || c.userId
+        var cid = this.candidates[i]._mid
         if (cid !== null && cid !== undefined) candIds[cid] = true
       }
       var list = []
       for (var j = 0; j < this.familyMembers.length; j++) {
         var m = this.familyMembers[j]
-        var mid = m.userId || m.id
+        var mid = m._mid
         if (mid !== null && mid !== undefined && candIds[mid]) continue
         list.push(m)
       }
@@ -745,6 +745,7 @@ export default {
       self.matchedMemberId = detail.matchedMemberId || null
       self.needBind = !!detail.needBind
       self.candidates = detail.candidates || []
+      for (var ci = 0; ci < self.candidates.length; ci++) self.candidates[ci]._mid = memberIdOf(self.candidates[ci])
       self.loadFamilyMembers()
       console.log('[报告入库] applyParseResult 后状态', {
         needBind: self.needBind,
@@ -757,8 +758,8 @@ export default {
         self.selectedMemberId = self.matchedMemberId
       }
       var best = self.candidates[0]
-      if (best && (best.id || best.userId)) {
-        self.selectedMemberId = best.id || best.userId
+      if (best && best._mid != null) {
+        self.selectedMemberId = best._mid
         self.selectedMemberObj = best
       }
       // 如果需要绑定且有候选成员,默认自动选中第一个
@@ -777,7 +778,10 @@ export default {
       var self = this
       getFamilyMemberList({ visibleOnly: true }).then(function(res) {
         if (res.code === 200) {
-          self.familyMembers = res.data || []
+          // 前端防御层:按 user_id 折叠同用户多行,isSelf 唯一;统一身份键 memberId
+          var list = normalizeMembers(res.data || [], { userId: uni.getStorageSync('userId') })
+          for (var i = 0; i < list.length; i++) list[i]._mid = memberIdOf(list[i])
+          self.familyMembers = list
         }
       }).catch(function() {})
     },
@@ -788,7 +792,7 @@ export default {
       }
     },
     selectAndClose(member) {
-      this.selectedMemberId = member.id || member.userId
+      this.selectedMemberId = member._mid != null ? member._mid : memberIdOf(member)
       this.selectedMemberObj = member
       this.createdMemberId = null
       this.needBind = false
@@ -827,7 +831,7 @@ export default {
         self.creatingMember = false
         if (res.code === 200) {
           var member = res.data || {}
-          self.selectedMemberId = member.id || member.userId
+          self.selectedMemberId = memberIdOf(member)
           self.selectedMemberObj = member
           self.createdMemberId = self.selectedMemberId
           self.needBind = false

+ 24 - 37
cfc-frontend/pages/health/report-list.vue

@@ -1,24 +1,16 @@
 <template>
   <view class="list-container" :style="{ '--font-scale': fontScale }">
-    <!-- 成员过滤 -->
-    <view class="member-filter">
-      <scroll-view class="filter-scroll" scroll-x="true" :show-scrollbar="false">
-        <view class="filter-chips">
-          <view class="chip" :class="{ 'chip-active': !selectedMemberId }" @click="selectAllMembers">
-            <text class="chip-text">全部</text>
-          </view>
-          <view
-            class="chip"
-            v-for="item in members"
-            :key="item.id"
-            :class="{ 'chip-active': selectedMemberId === item._filterId }"
-            @click="selectMember(item)"
-          >
-            <text class="chip-text">{{ item.nickname || '成员' }}</text>
-          </view>
-        </view>
-      </scroll-view>
-    </view>
+    <!-- 成员过滤(统一组件 MemberSelector:同一用户只出现一次,主键为 canonical memberId) -->
+    <MemberSelector
+      variant="minimal"
+      :members="members"
+      :selected-member-id="selectedMemberId"
+      :show-overview="true"
+      overview-label="全部"
+      :overview-active="selectedMemberId === null"
+      :show-self="false"
+      @overview="selectAllMembers"
+      @select="selectMember" />
 
     <!-- 跨家庭报告转移申请 -->
     <view class="transfer-wrap" v-if="transferIncoming.length > 0 || transferOutgoing.length > 0">
@@ -161,11 +153,14 @@
 <script>
 import { BrandColors } from '../../utils/brand-colors.js'
 import { parseDate } from '../../utils/format.js'
+import MemberSelector from '@/components/MemberSelector.vue'
+import { normalizeMembers, memberIdOf, resolveMemberId } from '@/utils/memberNormalize.js'
 import { getReportList, deleteReport, getFamilyMemberList, getCollectingReports, reassignReport, getReportTransferList, approveReportTransfer, denyReportTransfer } from '../../utils/api.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
 
 export default {
   mixins: [fontScaleMixin],
+  components: { MemberSelector },
   data() {
     return {
       reports: [],
@@ -313,28 +308,20 @@ export default {
       var self = this
       getFamilyMemberList({}).then(function(res) {
         if (res.code === 200 && Array.isArray(res.data)) {
-          // _filterId 预计算,供 :class 内联比较使用(小程序 :class 禁止方法调用)
-          var list = []
-          for (var i = 0; i < res.data.length; i++) {
-            var m = res.data[i]
-            list.push({
-              id: m.id,
-              userId: m.userId,
-              nickname: m.nickname,
-              avatar: m.avatar,
-              _filterId: m.id || m.userId
-            })
+          // 前端防御层:按 user_id 折叠同用户多行,isSelf 唯一;统一身份键 memberId
+          var list = normalizeMembers(res.data, { userId: uni.getStorageSync('userId') })
+          // _filterId 预计算,供调整归属面板 :class 内联比较使用(小程序 :class 禁止方法调用)
+          for (var i = 0; i < list.length; i++) {
+            list[i]._filterId = memberIdOf(list[i])
           }
           self.members = list
-          // 默认选中本人时,把 chip 高亮从 users.id 归一到本人的 family_members.id(_filterId):
+          // 默认选中本人时,把 chip 高亮从 users.id 归一到本人的 family_members.id(memberId):
           // 两个 id 后端返回同一份报告,数据已在 onLoad 拉取过,这里只修高亮、不重复请求
           if (self._selfUserId && self.selectedMemberId != null &&
               Number(self.selectedMemberId) === Number(self._selfUserId)) {
-            for (var j = 0; j < list.length; j++) {
-              if (list[j].userId != null && Number(list[j].userId) === Number(self._selfUserId)) {
-                self.selectedMemberId = list[j]._filterId
-                break
-              }
+            var resolved = resolveMemberId(list, self.selectedMemberId)
+            if (resolved != null) {
+              self.selectedMemberId = resolved
             }
           }
         }
@@ -352,7 +339,7 @@ export default {
       this.selectedAdjustMemberId = null
     },
     selectMember(item) {
-      var filterId = item._filterId
+      var filterId = item._filterId != null ? item._filterId : memberIdOf(item)
       if (this.selectedMemberId === filterId) return
       this.selectedMemberId = filterId
       this.loadData()

+ 5 - 2
cfc-frontend/pages/health/report-upload.vue

@@ -137,6 +137,7 @@
 </template>
 
 <script>
+import { normalizeMembers, memberIdOf } from '../../utils/memberNormalize.js'
 import { uploadReportOnly, getReportCollectStatus, getFamilyMemberList, requestReportTransfer } from '../../utils/api.js'
 import { pickImage } from '../../utils/image-picker.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
@@ -183,7 +184,7 @@ if (options.type === 'a2' || options.reportKind === 'a2') {
   methods: {
     resolveMemberId: function(member) {
       if (!member) return null
-      var raw = member.id || member.userId
+      var raw = memberIdOf(member)
       if (raw === 0 || raw === '0') return 0
       if (!raw) return null
       var id = parseInt(raw)
@@ -220,7 +221,9 @@ if (options.type === 'a2' || options.reportKind === 'a2') {
       var self = this
       getFamilyMemberList({ visibleOnly: true }).then(function(res) {
         var list = (res && res.data) || []
-        self.familyMembers = list instanceof Array ? list : []
+        // 前端防御层:按 user_id 折叠同用户多行,isSelf 唯一;统一身份键 memberId
+        var normalized = normalizeMembers(list instanceof Array ? list : [], { userId: uni.getStorageSync('userId') })
+        self.familyMembers = normalized
         if (!self.memberId && self.familyMembers.length === 1) {
           self.memberId = self.resolveMemberId(self.familyMembers[0])
         }