| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- /**
- * 家庭成员去重规范化工具(T25 统一成员选择器 · 层3 前端防御)
- *
- * 背景:
- * family_members 存量数据中同一 (user_id, family_id) 可能存在多条重复行,
- * 导致同一用户在前端被渲染成两项("本人" + "本人所对应的成员")。
- *
- * 重要边界(见设计文档 §一.3):
- * 本工具只是**前端防御层**。重复的 family_members 行各自挂着不同的外键数据
- * (任务/打卡/健康等),单纯折叠会"隐藏"其中一行的数据。
- * 真正的根治是数据层合并(T24-C),需单独授权执行。
- * 后端 FamilyMemberService.listMembers 已按 user_id 做确定性折叠,
- * 本工具用于兜住仍返回重复行的历史接口/前端缓存数据。
- *
- * 折叠规则:
- * 1. 按 user_id 分组折叠(userId 为空的"不能登录用户"各自独立成组)
- * 2. canonical 行优先级:isSelf 行 → status='active' → 外键数据最多 → id 最小
- * 3. 每个真实用户至多输出一行,且全列表 isSelf 唯一
- * 4. 统一身份键为 familyMemberId(family_members.id),userId 仅作折叠依据
- */
- /**
- * 取成员的统一身份键(familyMemberId)。
- * 后端 FamilyMemberVO 已下发 familyMemberId;历史接口缺失时回退到 id。
- */
- export function memberIdOf(member) {
- if (!member) return null
- if (member.familyMemberId !== null && member.familyMemberId !== undefined && member.familyMemberId !== '') {
- return member.familyMemberId
- }
- if (member.id !== null && member.id !== undefined && member.id !== '') {
- return member.id
- }
- return null
- }
- /** 成员是否为有效状态(active 优先;未带 status 字段的历史数据视为 active) */
- function isActive(member) {
- if (!member || member.status === null || member.status === undefined || member.status === '') {
- return true
- }
- return member.status === 'active'
- }
- /**
- * 成员"数据量"权重(用于 canonical 选取的第三级 tie-break)。
- * 后端若下发 fkCount(外键引用数)则直接采用;否则用可见的数据丰度做近似。
- */
- function dataWeight(member) {
- if (!member) return 0
- if (typeof member.fkCount === 'number' && !isNaN(member.fkCount)) return member.fkCount
- if (typeof member.refCount === 'number' && !isNaN(member.refCount)) return member.refCount
- var n = 0
- if (member.totalPoints) n += 1
- if (member.streakDays) n += 1
- if (member.lastInteractionAt) n += 1
- if (member.trustScore !== null && member.trustScore !== undefined) n += 1
- if (member.intimacyScore !== null && member.intimacyScore !== undefined) n += 1
- if (member.communicationScore !== null && member.communicationScore !== undefined) n += 1
- return n
- }
- function numericId(member) {
- var id = memberIdOf(member)
- var n = Number(id)
- return isNaN(n) ? Number.MAX_SAFE_INTEGER : n
- }
- /**
- * canonical 行比较器:返回 <0 表示 a 更优先。
- * 优先级:isSelf → status='active' → 数据量最多 → id 最小。
- */
- function compareCanonical(a, b) {
- var aSelf = a && a.isSelf ? 1 : 0
- var bSelf = b && b.isSelf ? 1 : 0
- if (aSelf !== bSelf) return bSelf - aSelf
- var aActive = isActive(a) ? 1 : 0
- var bActive = isActive(b) ? 1 : 0
- if (aActive !== bActive) return bActive - aActive
- var aWeight = dataWeight(a)
- var bWeight = dataWeight(b)
- if (aWeight !== bWeight) return bWeight - aWeight
- return numericId(a) - numericId(b)
- }
- function hasLoginUser(member) {
- return !!member && member.userId !== null && member.userId !== undefined && member.userId !== '' && Number(member.userId) !== 0
- }
- /**
- * 规范化成员列表。
- *
- * @param {Array} rawMembers 原始成员数组(后端 VO 或其变体)
- * @param {Object} [viewer] 当前查看者,形如 { userId, memberId }(可只给其一)
- * @returns {Array} 每个真实用户至多一条、isSelf 唯一的成员数组
- */
- export function normalizeMembers(rawMembers, viewer) {
- var list = rawMembers instanceof Array ? rawMembers : []
- if (list.length === 0) return []
- var viewerUserId = viewer && viewer.userId !== null && viewer.userId !== undefined && viewer.userId !== ''
- ? String(viewer.userId) : ''
- var viewerMemberId = viewer && viewer.memberId !== null && viewer.memberId !== undefined && viewer.memberId !== ''
- ? String(viewer.memberId) : ''
- // 1) 按 user_id 分组(保留首次出现顺序)
- var groups = []
- var groupIndex = {}
- for (var i = 0; i < list.length; i++) {
- var row = list[i]
- if (!row || typeof row !== 'object') continue
- var key
- if (hasLoginUser(row)) {
- key = 'u:' + String(row.userId)
- } else {
- // 不能登录用户(无 userId):每行独立,按自身 id 去重
- key = 'm:' + String(row.id !== undefined && row.id !== null ? row.id : ('idx:' + i))
- }
- if (groupIndex[key] === undefined) {
- groupIndex[key] = groups.length
- groups.push([])
- }
- groups[groupIndex[key]].push(row)
- }
- // 2) 每组选 canonical 行(shallow-clone 输出,避免污染父组件共享的成员数组)
- var result = []
- for (var g = 0; g < groups.length; g++) {
- var group = groups[g]
- var winner = group[0]
- if (group.length > 1) {
- for (var k = 1; k < group.length; k++) {
- if (compareCanonical(group[k], winner) < 0) {
- winner = group[k]
- }
- }
- }
- var copy = {}
- for (var key in winner) {
- if (Object.prototype.hasOwnProperty.call(winner, key)) {
- copy[key] = winner[key]
- }
- }
- result.push(copy)
- }
- // 3) isSelf 唯一化:优先认 viewer,其次保留后端标记的那一条
- var selfIndex = -1
- if (viewerMemberId) {
- for (var j = 0; j < result.length; j++) {
- if (String(memberIdOf(result[j])) === viewerMemberId) {
- selfIndex = j
- break
- }
- }
- }
- if (selfIndex < 0 && viewerUserId) {
- for (var j2 = 0; j2 < result.length; j2++) {
- if (hasLoginUser(result[j2]) && String(result[j2].userId) === viewerUserId) {
- selfIndex = j2
- break
- }
- }
- }
- if (selfIndex < 0) {
- for (var j3 = 0; j3 < result.length; j3++) {
- if (result[j3] && result[j3].isSelf) {
- selfIndex = j3
- break
- }
- }
- }
- for (var j4 = 0; j4 < result.length; j4++) {
- result[j4].isSelf = (j4 === selfIndex)
- }
- // 4) 补齐统一身份键 + 渲染用唯一 key
- var usedKeys = {}
- for (var j5 = 0; j5 < result.length; j5++) {
- var m = result[j5]
- var mid = memberIdOf(m)
- m.familyMemberId = mid
- m.memberId = mid
- var base = (m.source === 'parent' ? 'parent:' : 'member:') + String(mid)
- if (usedKeys[base]) {
- var n = 2
- while (usedKeys[base + '#' + n]) n++
- base = base + '#' + n
- }
- usedKeys[base] = true
- m.selectorKey = base
- }
- return result
- }
- /** 从(已规范化或未规范化的)成员列表中找出"本人"一行 */
- export function findSelfMember(members) {
- if (!(members instanceof Array)) return null
- for (var i = 0; i < members.length; i++) {
- if (members[i] && members[i].isSelf) return members[i]
- }
- return null
- }
- /** 从成员列表中排除"本人"行(用于"个人 + 各成员"形态,避免同一人出现两项) */
- export function membersExceptSelf(members) {
- if (!(members instanceof Array)) return []
- var out = []
- for (var i = 0; i < members.length; i++) {
- if (members[i] && !members[i].isSelf) out.push(members[i])
- }
- return out
- }
- export default normalizeMembers
|