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]); 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(); $db->prepare('DELETE FROM relations WHERE id = ? AND family_id = ?') ->execute([$id, $familyId]); $st = PersonRoster::syncKinForFamily($familyId); return (int)$st['dropped']; } /** 解除两人之间的某种关系(与 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(); } }