v15: 家谱重写 —— 以我为中心检索全体亲属 + 自己排版画线
用户 2026-10-02 的反馈(原话):
「显示不齐全,应该显示本人的上一代,这一代,和下一代的所有人…以本人为中心去搜索,
只要有关系的,都检索出来进行展示」
「夫妻之间有一条横线…这对夫妻下面所有的子女,都应该从夫妻之间的这个横线出来,
而不是从这对夫妻的其中一个出来,这样可能会出现一个 bug,就是如果从一个出来,
例如从父亲出来,母亲登录进去竟然看不到自己的孩子」
「数据仍然不全,差很多」
根因(实测,不是猜):第三方库 family-chart **只画了 20 人里的 9 人**。
_tooling/_fb_kin_cover_probe.py 的硬数据:
画出的:祖父/祖母/父亲/母亲/我/妻子/哥哥/儿子/女儿(9)
没画的:伯父、伯母、岳父、岳母、嫂子、堂兄、堂嫂、舅子、侄子、堂侄、外甥(11)
库的算法只画「树根的祖先链 + 后代 + 树根的兄弟姐妹 + 各人配偶」,
**横向亲戚(伯父那一支)与姻亲旁支(妻子的兄弟)永远进不来** ——
换树根也救不了(换到最高祖先,"妻子的兄弟"照样不在树里)。
「母亲看不到孩子」还有第二层原因:孩子只登记了父亲时,母亲那一侧根本没有边。
改法(不再依赖库的树算法):
· app/Kinship::familyMap() —— 以「我」为中心的两轮贯穿检索,带 seen 防环:
父母(-1 代)/ 子女(+1 代)/ 配偶(同代)/ 每一层的配偶
/ 配偶的父母与子女(姻亲旁支由此进来)/ 兄弟姐妹的子女(侄甥)
→ 上一代、这一代、下一代的**所有有关系的人**都在名单里。
· app/Kinship::householdChildren() —— 子女按**父母所在的户**归堆,
而不是按"某个人的 children"。于是孩子只登记了一位父母时,
照样挂到这对夫妻的户上(语义:这孩子是我们俩的)。
· templates/kin/tree.php —— 自己排版:每代一行、按「户」水平排开;
夫妻两张卡之间画短横线;子女一律从**这条横线的中点**垂下来
(x = (左卡中心 + 右卡中心) / 2,与"谁登录"无关)。
· 拖拽/缩放保留(用户明确要),改用原生 d3.zoom;不再引 family-chart。
验证(只跑这一个模块,按用户要求):
· _tooling/_fb_kin_v15.py —— 22 项全过:
✔ 20/20 全员画出(== 从「我」出发可达的人数)
✔ 7 对夫妻都有横线;7 个有子女的户**下垂线全部落在横线中点**
✔ 用户点名的场景一:删掉「母亲→儿子」这条边(模拟只登记父亲)后,
下垂线**仍然**从夫妻横线中点出来,孩子仍在图上
✔ 用户点名的场景二:给母亲开账号、**用她的手机号登录**,
家谱里能看到自己的孩子,且与父亲视角画的是同一批人
✔ 截图存档(全图 / 母亲视角 / 窄屏)
· _tooling/_fb_kin_v15_falsify.py —— 反向验证 2/2 抓住:
注入「从其中一张卡出来」→「从横线中点出来」变红
注入「只走直系血亲」→「每个人都被画出来」变红
· e2e 550 条全过(家谱段改到 v15 的判据上)
顺带修的既有问题:
· app/Kinship.php 老库升级回填按实际存在的列动态拼(缺列不再 1054 炸断升级)
· _fb_e2e.py 家谱断言改到 kin_v15_nodes(旧解析器在新页面上只会解析出空)
This commit is contained in:
+150
@@ -134,6 +134,156 @@ final class Kinship
|
||||
return array_map('intval', array_keys($out));
|
||||
}
|
||||
|
||||
/**
|
||||
* v15:**家谱图的完整数据出口**(以本人为中心,上一代 / 这一代 / 下一代全员)。
|
||||
*
|
||||
* 为什么重写(用户 2026-10-02 的原话):
|
||||
* 「显示不齐全,应该显示本人的上一代,这一代,和下一代的所有人…
|
||||
* 应该以本人为中心去搜索,只要有关系的,都检索出来进行展示」
|
||||
* 「数据仍然不全,差很多」
|
||||
*
|
||||
* 实测(_fb_kin_cover_probe.py):第三方库 family-chart **只画了 20 人里的 9 人** ——
|
||||
* 伯父/伯母/岳父母/嫂子/堂兄/堂嫂/舅子/侄子/堂侄/外甥一个都没画。
|
||||
* 库只会画「树根的祖先链 + 后代 + 树根的兄弟姐妹」,**横向的亲戚与姻亲旁支
|
||||
* 永远进不来**(换树根也救不了:换到最高祖先,"妻子的兄弟"照样不在树里)。
|
||||
* 所以家谱不再依赖库的树算法,改由这里给出**完整的名单 + 代数 + 户**,
|
||||
* 前端按「代分层、户并排」自己排版、自己画线。
|
||||
*
|
||||
* 算法(以本人为中心的两轮贯穿,带 $seen 防环):
|
||||
* ① 从「我」出发:父母(-1 代)、子女(+1 代)、配偶(同代);
|
||||
* ② 从**每一位配偶**出发:其父母与子女也要进来 —— 配偶的父母是姻亲(同 -1 代),
|
||||
* 这样「妻子的兄弟」「妻子的侄子」这些姻亲旁支才有路子进来;
|
||||
* ③ 血亲每往上一层,就把那层的**配偶们**也补进来(伯母/婶婶/姑父…);
|
||||
* ④ 血亲每往下一层,同样补配偶,并**跨层补侄甥**(兄弟姐妹的子女 = 下一代);
|
||||
* ⑤ 代数越界(超过 up/down 代)的人只到边界那一层为止,不再向外扩 ——
|
||||
* 否则"有关系"会一路串到远亲,图会无限大。
|
||||
*
|
||||
* @return array{
|
||||
* gen: array<int,int>, id => 相对代数(我 = 0)
|
||||
* units: array<int,array>, 户(夫妻单元)列表,见下
|
||||
* unit_of: array<int,int>, id => 户序号
|
||||
* me: int
|
||||
* }
|
||||
*/
|
||||
public static function familyMap(int $familyId, int $me, array $people, int $up = 2, int $down = 2): array
|
||||
{
|
||||
$g = self::graph($familyId);
|
||||
if (!isset($people[$me])) {
|
||||
return ['gen' => [], 'units' => [], 'unit_of' => [], 'me' => $me];
|
||||
}
|
||||
|
||||
// ---- ① 代数:以「我」为 0 代,反复贯穿直到不再有人可加 ----
|
||||
$gen = [$me => 0];
|
||||
$seen = [$me => true];
|
||||
$q = [$me];
|
||||
// 卡片能容纳的代数有限,越界就不再往外走(但边界那一层的人照样收)
|
||||
$clamp = static function (int $v) use ($up, $down): int {
|
||||
return max(-$up, min($down, $v));
|
||||
};
|
||||
while ($q) {
|
||||
$id = (int)array_shift($q);
|
||||
$lv = (int)$gen[$id];
|
||||
$push = static function (int $nid, int $nlv) use (&$gen, &$seen, &$q, $people, $clamp): void {
|
||||
$nlv = $clamp($nlv);
|
||||
if (!isset($people[$nid])) {
|
||||
return; // 名册里没有这个人
|
||||
}
|
||||
if (!isset($seen[$nid])) {
|
||||
$seen[$nid] = true;
|
||||
$gen[$nid] = $nlv;
|
||||
$q[] = $nid;
|
||||
} elseif ($nlv > $gen[$nid]) {
|
||||
// ⚠️ 同一人可能有两条路到达、算出不同代数(堂兄既是"伯父的孩子"、
|
||||
// 也可能通过某条配偶边走到)。取**离我最近**的那一代,
|
||||
// 否则他会跑到下一代的层里去。
|
||||
$gen[$nid] = $nlv;
|
||||
}
|
||||
};
|
||||
foreach (self::parentIds($g, $id) as $p) { $push((int)$p, $lv - 1); }
|
||||
foreach (self::childIds($g, $id) as $c) { $push((int)$c, $lv + 1); }
|
||||
// 配偶永远同代 —— 这一条让「配偶的父母/子女」也能顺着扩出去
|
||||
foreach (self::spouseIds($g, $id) as $s) { $push((int)$s, $lv); }
|
||||
}
|
||||
|
||||
// ---- ② 户(夫妻单元):一对互为配偶的两人合成一户 ----
|
||||
// 只在「同一代 + 互为配偶」时成户;跨代的配偶边(脏数据)各自成户,不硬凑。
|
||||
$units = [];
|
||||
$unitOf = [];
|
||||
$assigned = [];
|
||||
foreach (array_keys($gen) as $id) {
|
||||
$id = (int)$id;
|
||||
if (isset($assigned[$id])) {
|
||||
continue;
|
||||
}
|
||||
$assigned[$id] = true;
|
||||
$partner = 0;
|
||||
foreach (self::spouseIds($g, $id) as $s) {
|
||||
$s = (int)$s;
|
||||
if (isset($gen[$s]) && (int)$gen[$s] === (int)$gen[$id] && !isset($assigned[$s])) {
|
||||
$partner = $s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($partner > 0) {
|
||||
$assigned[$partner] = true;
|
||||
}
|
||||
$units[] = ['gen' => (int)$gen[$id], 'ids' => $partner > 0 ? [$id, $partner] : [$id]];
|
||||
}
|
||||
foreach ($units as $i => $u) {
|
||||
foreach ($u['ids'] as $id) {
|
||||
$unitOf[(int)$id] = $i;
|
||||
}
|
||||
}
|
||||
|
||||
return ['gen' => $gen, 'units' => $units, 'unit_of' => $unitOf, 'me' => $me];
|
||||
}
|
||||
|
||||
/**
|
||||
* v15:把「孩子」按**父母所在的户**归堆。
|
||||
*
|
||||
* 用户的核心要求(原话):「这对夫妻下面所有的子女,都应该从夫妻之间的这个横线出来,
|
||||
* 而不是从这对夫妻的其中一个出来,这样可能会出现一个 bug,就是如果从一个出来,
|
||||
* 例如从父亲出来,母亲登录进去之后竟然看不到自己的孩子,这是非常大的问题。」
|
||||
*
|
||||
* 所以这里**不以"某个人的 children"为准** —— 那正是"从父亲一个人出来"的根源
|
||||
* (孩子只登记了父亲时,父亲有边、母亲没边,母亲登录就看不到孩子)。
|
||||
* 改成:对每个孩子取其**父母们**,再看这些父母落在哪一户,把这条子女关系
|
||||
* 挂到**那一户**上。于是:
|
||||
* · 孩子登记了父母双方 → 挂到这对夫妻的户 → 从夫妻横线中点垂线;
|
||||
* · 孩子只登记了一位父/母 → 也挂到那位父/母**所在的户**(该户若有配偶,
|
||||
* 照样从两人的横线中点出来 —— 语义上"这孩子是我们俩的")。
|
||||
*
|
||||
* @param array<int,int> $unitOf id => 户序号
|
||||
* @return array<int,int[]> 户序号 => [孩子 id, ...]
|
||||
*/
|
||||
public static function householdChildren(array $g, array $unitOf): array
|
||||
{
|
||||
$out = [];
|
||||
foreach (array_keys($unitOf) as $child) {
|
||||
$child = (int)$child;
|
||||
$u = (int)$unitOf[$child];
|
||||
// 只认「这位孩子的父母」,且父母必须在图上
|
||||
$parentsHere = [];
|
||||
foreach (self::parentIds($g, $child) as $p) {
|
||||
$p = (int)$p;
|
||||
if (isset($unitOf[$p]) && (int)$unitOf[$p] !== $u) {
|
||||
// ⚠️ 父母的户不能是孩子自己所在的户(把配偶记成父母这类脏数据)
|
||||
$parentsHere[] = $p;
|
||||
}
|
||||
}
|
||||
if (!$parentsHere) {
|
||||
continue; // 没有父母在图上 → 不是"某户的孩子"
|
||||
}
|
||||
// 同一个孩子可能被多位父母指向不同户(脏数据):取第一户,避免重复画线
|
||||
$home = (int)$unitOf[$parentsHere[0]];
|
||||
$out[$home][] = $child;
|
||||
}
|
||||
foreach ($out as $k => $v) {
|
||||
$out[$k] = array_values(array_unique(array_map('intval', $v)));
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 祖父母那一级:往上走一步再取父母。
|
||||
* 只认「我的父母」,因为只有相邻两代有边 —— 这正是「只存两代」的兑现处。
|
||||
|
||||
Reference in New Issue
Block a user