false, 'error' => '不能把自己设成自己的父母。']; } if (self::wouldCycle($familyId, $parentId, $childId)) { return ['ok' => false, 'error' => '这样会让两个人互为祖辈,关系说不通,已拒绝。']; } // 同一位父母只能有一个「父亲」和一个「母亲」位 $db = Database::get(); $st = $db->prepare( 'DELETE FROM relations WHERE family_id = ? AND to_id = ? AND kind = "parent" AND from_role = ?' ); $st->execute([$familyId, $childId, $role]); $st = $db->prepare( 'INSERT INTO relations (family_id, from_id, to_id, kind, from_role) VALUES (?,?,?,"parent",?)' ); $st->execute([$familyId, $parentId, $childId, $role]); // 家谱变了 ⇒ 全家每个账号的 kin 名册条目按新树对齐(v7,三处口径一致) PersonRoster::syncKinForFamily($familyId); return ['ok' => true, 'error' => '']; } /** 互为配偶(对称,只存一条;若已存在反向的记录则视为重复) */ public static function addSpouse(int $familyId, int $a, int $b): array { if ($a === $b) { return ['ok' => false, 'error' => '不能把自己设成自己的配偶。']; } $db = Database::get(); $st = $db->prepare( 'SELECT id FROM relations WHERE family_id = ? AND kind = "spouse" AND ((from_id = ? AND to_id = ?) OR (from_id = ? AND to_id = ?))' ); $st->execute([$familyId, $a, $b, $b, $a]); if ($st->fetchColumn()) { // 已存在 ≠ 什么都不做:名册那边可能还没跟上(老数据/升级前建的边),补一次 PersonRoster::syncKinForFamily($familyId); return ['ok' => true, 'error' => '']; } $st = $db->prepare( 'INSERT INTO relations (family_id, from_id, to_id, kind) VALUES (?,?,?,"spouse")' ); $st->execute([$familyId, $a, $b]); // 重新加上了配偶 ⇒ 之前"解除过"的标记要撤掉, // 否则互补推导会一直跳过这一对(连带影响他们的孩子只挂一位家长)。 self::clearPairUndone($familyId, $a, $b); PersonRoster::syncKinForFamily($familyId); return ['ok' => true, 'error' => '']; } /** * 解除某条关系(按 id)。 * * ⚠️ v7 起「家谱树 ⇒ 名册自动同步」:边没了,树上够不着的人的 kin 名册条目 * 也跟着撤(可能不止这一对——比如解开「父子」,孩子就从配偶的名册里一起消失, * 因为树是从配偶那边绕过我来展开的)。self/shared/seeded 的条目绝不碰。 * * @return int 撤掉的 kin 名册条目数(路由层用它把话说全) */ public static function remove(int $familyId, int $id): int { $db = Database::get(); // ⚠️ 删之前先读出来:如果是**配偶**边,要把它记进「显式解除」名单, // 否则 Kinship::graph() 的互补推导会凭"共同孩子"把两人又配回夫妻, // 用户看到的现象是「解除了却还在」。 $row = $db->prepare('SELECT from_id, to_id, kind FROM relations WHERE id = ? AND family_id = ?'); $row->execute([$id, $familyId]); $r = $row->fetch(\PDO::FETCH_ASSOC); if ($r && $r['kind'] === 'spouse') { self::markPairUndone($familyId, (int)$r['from_id'], (int)$r['to_id']); } $db->prepare('DELETE FROM relations WHERE id = ? AND family_id = ?') ->execute([$id, $familyId]); $st = PersonRoster::syncKinForFamily($familyId); return (int)$st['dropped']; } /** * 记下「这一对已经被人为解除过夫妻关系」。 * * 为什么要记:家谱的关系是**推导**出来的(`Kinship::graph()` 会把 * 「同一个孩子的两位家长」认成夫妻、把「唯一配偶」补成另一位家长)。 * 于是「解除配偶」如果只删库里的边,推导会立刻用共同孩子把两人配回来 —— * 用户点了解除、刷新还在,等于功能失效。 * 记进 settings 的 `kin_undone_pairs`(不动表结构),推导时跳过这些对。 */ public static function markPairUndone(int $familyId, int $a, int $b): void { if ($a <= 0 || $b <= 0 || $a === $b) { return; } $key = $a < $b ? "$a-$b" : "$b-$a"; $list = self::undonePairs($familyId); if (!in_array($key, $list, true)) { $list[] = $key; Setting::set($familyId, 'kin_undone_pairs', implode(',', $list)); } } /** 已被显式解除过的夫妻对('小id-大id' 形式),推导时跳过 */ public static function undonePairs(int $familyId): array { $v = (string)Setting::get($familyId, 'kin_undone_pairs', ''); if ($v === '') { return []; } return array_values(array_filter(array_map('trim', explode(',', $v)), static fn($x) => $x !== '')); } /** 这对是否被显式解除过(顺序无关) */ public static function isPairUndone(int $familyId, int $a, int $b): bool { if ($a <= 0 || $b <= 0 || $a === $b) { return false; } $key = $a < $b ? "$a-$b" : "$b-$a"; return in_array($key, self::undonePairs($familyId), true); } /** * 重新建立关系时,把它从「显式解除」名单里抹掉 * (不然用户重新加了配偶,推导还是把它当解除过 → 又少一处关系)。 */ public static function clearPairUndone(int $familyId, int $a, int $b): void { if ($a <= 0 || $b <= 0 || $a === $b) { return; } $key = $a < $b ? "$a-$b" : "$b-$a"; $list = array_values(array_filter(self::undonePairs($familyId), static fn($x) => $x !== $key)); Setting::set($familyId, 'kin_undone_pairs', implode(',', $list)); } /** 解除两人之间的某种关系(与 remove() 同样的名册同步语义;当前无其他调用方) */ public static function detach(int $familyId, int $personId, string $kind, string $role = ''): void { $sql = 'DELETE FROM relations WHERE family_id = ? AND to_id = ? AND kind = ? AND from_role = ?'; Database::get()->prepare($sql)->execute([$familyId, $personId, $kind, $role]); PersonRoster::syncKinForFamily($familyId); } /** 某人身上挂着的全部关系(详情页编辑用) */ public static function forPerson(int $familyId, int $personId): array { $st = Database::get()->prepare( 'SELECT r.*, pf.name AS from_name, pt.name AS to_name FROM relations r JOIN persons pf ON pf.id = r.from_id JOIN persons pt ON pt.id = r.to_id WHERE r.family_id = ? AND (r.from_id = ? OR r.to_id = ?) ORDER BY r.kind, r.id' ); $st->execute([$familyId, $personId, $personId]); return $st->fetchAll(); } /** * 防环:若把 $maybeAncestor 挂到 $node 之上,会不会绕回自己? * 做法是顺着 parent 边从 $maybeAncestor 往上走,看能否走到 $node。 */ private static function wouldCycle(int $familyId, int $maybeAncestor, int $node): bool { $st = Database::get()->prepare( 'SELECT from_id FROM relations WHERE family_id = ? AND to_id = ? AND kind = "parent"' ); $stack = [$maybeAncestor]; $seen = []; while ($stack) { $cur = array_pop($stack); if ($cur === $node) { return true; } if (isset($seen[$cur])) { continue; } $seen[$cur] = true; $st->execute([$familyId, $cur]); foreach ($st->fetchAll(\PDO::FETCH_COLUMN) as $up) { $stack[] = (int)$up; } } return false; } /** 全家的关系条数(设置页/统计用) */ public static function count(int $familyId): int { $st = Database::get()->prepare('SELECT COUNT(*) FROM relations WHERE family_id = ?'); $st->execute([$familyId]); return (int)$st->fetchColumn(); } }