[parentId => 'father'|'mother'] $children = []; // parentId => [childId, ...] $spouses = []; // id => [spouseId, ...] $st = Database::get()->prepare( 'SELECT from_id, to_id, kind, from_role FROM relations WHERE family_id = ?' ); $st->execute([$familyId]); foreach ($st->fetchAll() as $r) { $f = (int)$r['from_id']; $t = (int)$r['to_id']; if ($r['kind'] === 'parent') { $role = $r['from_role'] === 'mother' ? 'mother' : 'father'; $parents[$t][$f] = $role; $children[$f][] = $t; } else { $spouses[$f][] = $t; $spouses[$t][] = $f; } } foreach ($children as $k => $v) { $children[$k] = array_values(array_unique($v)); } return ['parents' => $parents, 'children' => $children, 'spouses' => $spouses]; } /** 某人的父母(father / mother 两个键,可能缺) */ public static function parents(array $g, int $id): array { $out = ['father' => null, 'mother' => null]; foreach ($g['parents'][$id] ?? [] as $pid => $role) { $out[$role] = (int)$pid; } return $out; } /** 父母的 id 列表(不含 null),判断「跟我有关系」时用 */ public static function parentIds(array $g, int $id): array { $out = []; foreach (self::parents($g, $id) as $pid) { if ($pid !== null) { $out[] = $pid; } } return $out; } public static function spouseIds(array $g, int $id): array { return $g['spouses'][$id] ?? []; } /** 兄弟姐妹:与「我」共享至少一位父母的人(不含自己) */ public static function siblingIds(array $g, int $id): array { $sib = []; foreach ($g['parents'][$id] ?? [] as $pid => $_) { foreach ($g['children'][$pid] ?? [] as $c) { if ((int)$c !== $id) { $sib[(int)$c] = true; } } } return array_keys($sib); } /** 子女 */ public static function childIds(array $g, int $id): array { return array_map('intval', $g['children'][$id] ?? []); } /** * 祖父母那一级:往上走一步再取父母。 * 只认「我的父母」,因为只有相邻两代有边 —— 这正是「只存两代」的兑现处。 */ public static function grandparents(array $g, int $id): array { $out = ['paternal' => ['father' => null, 'mother' => null], 'maternal' => ['father' => null, 'mother' => null]]; $p = self::parents($g, $id); if ($p['father']) { $out['paternal'] = self::parents($g, $p['father']); } if ($p['mother']) { $out['maternal'] = self::parents($g, $p['mother']); } return $out; } /** * 我该怎么称呼对方 —— 整个系统的「以我为中心」就落在这个函数上。 * * 推导顺序:自己 → 配偶 → 父母/子女 → 兄弟姐妹 → 父母的兄弟姐妹 → * 配偶的父母 → 祖父母/孙辈 → 姻亲 → 兜底「亲属」 */ public static function label(array $g, int $me, int $other, array $people): string { if ($me === $other) { return '我'; } $gender = strtolower((string)($people[$other]['gender'] ?? 'x')); $isMale = $gender === 'm'; $isKnown = $gender === 'm' || $gender === 'f'; // ---- 配偶 ---- if (in_array($other, self::spouseIds($g, $me), true)) { return self::spouseLabel($g, $me, $isMale); } // ---- 直系:父母 / 子女 ---- $myParents = self::parents($g, $me); foreach (['father', 'mother'] as $role) { if ($myParents[$role] === $other) { return $role === 'father' ? '父亲' : '母亲'; } } if (in_array($other, self::childIds($g, $me), true)) { return self::rankLabel($g, $people, $me, $other, $isMale, $isKnown); } // ---- 兄弟姐妹(含排行)---- foreach (self::siblingIds($g, $me) as $sib) { if ($sib === $other) { // compareRank(我, 他) < 0 表示他的排行键更小 —— 也就是他比我年长。 // 方向很容易写反:这里比的是「他 vs 我」,不是「我 vs 他」。 $heIsOlder = self::compareRank($people, $other, $me) < 0; if ($isKnown) { if ($isMale) { return $heIsOlder ? '哥哥' : '弟弟'; } return $heIsOlder ? '姐姐' : '妹妹'; } return $heIsOlder ? '哥哥' : '弟弟'; } } // ---- 父母的兄弟姐妹:伯/叔/姑/舅/姨 ---- $uncle = self::uncleLabel($g, $people, $me, $other); if ($uncle !== null) { return $uncle; } // ---- 配偶的父母:公公婆婆 / 岳父岳母 ---- foreach (self::spouseIds($g, $me) as $sp) { $spParents = self::parents($g, $sp); foreach (['father', 'mother'] as $role) { if ($spParents[$role] === $other) { $iAmMale = strtolower((string)($people[$me]['gender'] ?? 'x')) === 'm'; if ($role === 'father') { return $iAmMale ? '岳父' : '公公'; } return $iAmMale ? '岳母' : '婆婆'; } } } // ---- 祖父母 / 外祖父母 ---- $gp = self::grandparents($g, $me); foreach (['paternal' => '爷爷', 'maternal' => '外公'] as $side => $maleWord) { $femaleWord = $side === 'paternal' ? '奶奶' : '外婆'; if (($gp[$side]['father'] ?? null) === $other) { return $maleWord; } if (($gp[$side]['mother'] ?? null) === $other) { return $femaleWord; } } // ---- 孙辈 / 曾孙辈 ---- // 只往下走两级:孙辈(子女的子女)与曾孙辈(子女的孙辈)。 // 再往下就不必细分了,统一「后代」。 foreach (self::childIds($g, $me) as $child) { foreach (self::childIds($g, $child) as $gc) { if ($gc === $other) { if (!$isKnown) { return '孙辈'; } return $isMale ? '孙子' : '孙女'; } if (in_array($other, self::childIds($g, $gc), true)) { if (!$isKnown) { return '后代'; } return $isMale ? '曾孙' : '曾孙女'; } } } // ---- 姻亲:配偶的兄弟姐妹 / 兄弟姐妹的配偶 ---- foreach (self::spouseIds($g, $me) as $sp) { if (in_array($other, self::siblingIds($g, $sp), true)) { return $isKnown ? ($isMale ? '大舅子/小舅子' : '大姨子/小姨子') : '姻亲'; } } foreach (self::siblingIds($g, $me) as $sib) { if (in_array($other, self::spouseIds($g, $sib), true)) { return $isKnown ? ($isMale ? '姐夫/妹夫' : '嫂子/弟媳') : '姻亲'; } } // ---- 表亲堂亲:父母的兄弟姐妹的子女 ---- foreach (self::parentSiblings($g, $me) as $ps) { if (in_array($other, self::childIds($g, $ps), true)) { return '表亲'; } } return '亲属'; } /** 配偶的称呼:按「我的性别」定,因为中文里丈夫/妻子是从自己视角说的 */ private static function spouseLabel(array $g, int $me, bool $otherIsMale): string { if ($otherIsMale) { return '丈夫'; } return '妻子'; } /** * 子女的排行称呼:长子/次子/三子…,长女/次女/三女… * 优先用 persons.birth_rank;没填就按生日或 id 推。 * * ⚠️ $isMale 必须由调用方按 gender 传入 —— 早期这里写死成「子」, * 结果女儿一律显示成「次子」,是很容易漏的一类 bug。 * 性别未知时退化成中性的「孩子」,不要瞎猜成儿子。 */ private static function rankLabel(array $g, array $people, int $parent, int $child, bool $isMale, bool $known): string { $base = $known ? ($isMale ? '子' : '女') : '孩子'; $sibs = self::childIds($g, $parent); if (count($sibs) <= 1) { return $known ? ($isMale ? '儿子' : '女儿') : '孩子'; } $rank = self::rankOf($people, $sibs, $child); $prefix = ['', '长', '次', '三', '四', '五', '六', '七', '八', '九'][$rank] ?? ($rank . ''); return $prefix === '' ? $base : $prefix . $base; } /** 我在兄弟姐妹里排第几(1 起) */ private static function rankOf(array $people, array $sibs, int $id): int { $sorted = $sibs; usort($sorted, function ($a, $b) use ($people) { $ra = self::rankValue($people, $a); $rb = self::rankValue($people, $b); return $ra <=> $rb; }); $pos = array_search($id, $sorted, true); return $pos === false ? 1 : $pos + 1; } /** 排序用的键:显式 birth_rank 优先,否则用生日,再否则用 id */ private static function rankValue(array $people, int $id): string { $p = $people[$id] ?? []; if (!empty($p['birth_rank'])) { return sprintf('%06d', (int)$p['birth_rank']); } if (!empty($p['birthday'])) { return '5' . (string)$p['birthday']; } return '9' . sprintf('%09d', $id); } /** 谁是哥哥谁是弟弟:比较排行键 */ private static function compareRank(array $people, int $a, int $b): int { return strcmp(self::rankValue($people, $a), self::rankValue($people, $b)); } /** 父母的兄弟姐妹(伯父/叔叔/姑姑/舅舅/姨妈) */ private static function uncleLabel(array $g, array $people, int $me, int $other): ?string { $sibs = []; $myParents = self::parents($g, $me); $paternal = $myParents['father'] !== null; foreach (['father' => true, 'mother' => false] as $role => $isPat) { $pid = $myParents[$role]; if ($pid === null) { continue; } foreach (self::siblingIds($g, $pid) as $sib) { $sibs[$sib] = ['side' => $isPat ? 'paternal' : 'maternal', 'parent' => $pid]; } } if (!isset($sibs[$other])) { return null; } $side = $sibs[$other]['side']; $parent = $sibs[$other]['parent']; $gender = strtolower((string)($people[$other]['gender'] ?? 'x')); $isMale = $gender === 'm'; $older = self::compareRank($people, $other, $parent) < 0; // 比我的父母年长 if ($side === 'paternal') { if ($isMale) { return $older ? '伯父' : '叔叔'; } return $gender === 'f' ? '姑姑' : '姑姑'; } if ($isMale) { return '舅舅'; } return $gender === 'f' ? '姨妈' : '姨妈'; } /** 父母的兄弟姐妹(不含配偶那边的) */ private static function parentSiblings(array $g, int $me): array { $out = []; foreach (self::parents($g, $me) as $pid) { if ($pid === null) { continue; } foreach (self::siblingIds($g, $pid) as $sib) { $out[] = $sib; } } return $out; } // ================= 家谱渲染所需的结构化数据 ================= /** * 以 $me 为中心,向上向下各展开若干代,产出可渲染的家谱树。 * * 返回结构: * [ * 'generations' => [ -2 => [节点...], -1 => [...], 0 => [...], 1 => [...] ], * 'nodes' => [ id => ['id','name','label','rel'][...] ], * ] * 其中 label 是「我该怎么称呼他」,rel 是「他是我的谁」(用于画连接线)。 */ public static function tree(array $g, int $me, array $people, int $up = 3, int $down = 3): array { $gen = []; // id => 相对我的代数(我 = 0,父母 = -1,子女 = +1) $seen = []; $queue = [[$me, 0]]; $seen[$me] = true; while ($queue) { [$id, $level] = array_shift($queue); // 配偶永远同代 foreach (self::spouseIds($g, $id) as $sp) { if (!isset($seen[$sp])) { $seen[$sp] = true; $gen[$sp] = $level; $queue[] = [$sp, $level]; } } // 往上 if ($level > -$up) { foreach (self::parents($g, $id) as $pid) { if ($pid !== null && !isset($seen[$pid])) { $seen[$pid] = true; $gen[$pid] = $level - 1; $queue[] = [$pid, $level - 1]; } } } // 往下 if ($level < $down) { foreach (self::childIds($g, $id) as $cid) { if (!isset($seen[$cid])) { $seen[$cid] = true; $gen[$cid] = $level + 1; $queue[] = [$cid, $level + 1]; } } } } $gen[$me] = 0; $generations = []; $nodes = []; foreach ($gen as $id => $level) { if (!isset($people[$id])) { continue; } $label = self::label($g, $me, $id, $people); $nodes[$id] = [ 'id' => $id, 'name' => (string)$people[$id]['name'], 'label' => $label, 'rel' => self::relationWord($g, $me, $id), 'avatar'=> (string)($people[$id]['avatar'] ?? ''), 'gender'=> (string)($people[$id]['gender'] ?? 'x'), 'ph' => (int)($people[$id]['is_placeholder'] ?? 0) === 1, 'person'=> $people[$id], ]; $generations[$level][] = $id; } ksort($generations); foreach ($generations as &$ids) { // 同代内排序:先配偶,再按排行 usort($ids, function ($a, $b) use ($people) { return strcmp(self::rankValue($people, $a), self::rankValue($people, $b)); }); } unset($ids); return ['generations' => $generations, 'nodes' => $nodes, 'gen' => $gen]; } /** 关系类型的「英文键」——给前端画线用,不受中文称呼影响 */ public static function relationWord(array $g, int $me, int $other): string { if ($me === $other) { return 'self'; } if (in_array($other, self::spouseIds($g, $me), true)) { return 'spouse'; } if (self::parents($g, $me)['father'] === $other || self::parents($g, $me)['mother'] === $other) { return 'parent'; } if (in_array($other, self::childIds($g, $me), true)) { return 'child'; } if (in_array($other, self::siblingIds($g, $me), true)) { return 'sibling'; } foreach (self::childIds($g, $me) as $c) { foreach (self::childIds($g, $c) as $gc) { if ($gc === $other || in_array($other, self::childIds($g, $gc), true)) { return 'grandchild'; } } } foreach (self::grandparents($g, $me) as $side) { if ($side['father'] === $other || $side['mother'] === $other) { return 'grandparent'; } } return 'kin'; } /** 把 id 列表变成 名字 => 称谓 的列表,供页面直接渲染 */ public static function describe(array $g, int $me, array $ids, array $people): array { $out = []; foreach ($ids as $id) { if (isset($people[$id])) { $out[] = [ 'id' => (int)$id, 'name' => (string)$people[$id]['name'], 'label' => self::label($g, $me, (int)$id, $people), ]; } } return $out; } }