prepare( 'INSERT IGNORE INTO person_groups (family_id, name, position, is_builtin) VALUES (?, ?, ?, 1)' ); foreach (Schema::DEFAULT_GROUPS as $name => $pos) { $st->execute([$familyId, $name, $pos]); } } /** 全部分组,id => 行(按默认顺序) */ public static function byId(int $familyId): array { $st = Database::get()->prepare( 'SELECT * FROM person_groups WHERE family_id = ? ORDER BY position ASC, id ASC' ); $st->execute([$familyId]); $out = []; foreach ($st->fetchAll() as $r) { $out[(int)$r['id']] = $r; } return $out; } /** * 按**当前用户自定义的顺序**返回分组(用户没自定义过就按默认位置)。 * * 自定义顺序里没提到的分组(用户排序之后才新建的)一律追加到最后 —— * 这样「新建一个分组」不会因为它不在旧顺序里就消失不见。 */ public static function orderFor(int $familyId, int $viewerId = 0): array { $rest = self::byId($familyId); if (!$rest) { return []; } if ($viewerId > 0) { $raw = (string)UserSetting::get($viewerId, 'group_order', ''); $saved = $raw === '' ? null : json_decode($raw, true); if (is_array($saved) && $saved) { $ordered = []; foreach ($saved as $id) { $id = (int)$id; if (isset($rest[$id])) { $ordered[$id] = $rest[$id]; unset($rest[$id]); } } foreach ($rest as $id => $g) { $ordered[$id] = $g; } return array_values($ordered); } } return array_values($rest); } /** 保存「我」的分组顺序(只影响我自己) */ public static function saveOrder(int $familyId, int $viewerId, array $ids): void { $valid = self::byId($familyId); $clean = []; foreach ($ids as $id) { $id = (int)$id; if (isset($valid[$id]) && !in_array($id, $clean, true)) { $clean[] = $id; } } // 没提到的补齐到末尾 —— 保存完整顺序,读回来才不会因为"缺项"而错位 foreach (array_keys($valid) as $id) { if (!in_array($id, $clean, true)) { $clean[] = $id; } } UserSetting::set($viewerId, 'group_order', json_encode($clean, JSON_UNESCAPED_UNICODE)); } /** 分组名(找不到就返回空串,调用方自己决定回退文案) */ public static function name(int $familyId, ?int $id): string { if (!$id) { return ''; } $st = Database::get()->prepare('SELECT name FROM person_groups WHERE family_id = ? AND id = ?'); $st->execute([$familyId, $id]); return (string)($st->fetchColumn() ?: ''); } /** * 新增一个分组。 * @return array{ok:bool,id?:int,error?:string} */ public static function add(int $familyId, string $name): array { $name = trim($name); $bad = self::validateName($name); if ($bad !== '') { return ['ok' => false, 'error' => $bad]; } $st = Database::get()->prepare( 'SELECT COUNT(*) FROM person_groups WHERE family_id = ? AND name = ?' ); $st->execute([$familyId, $name]); if ((int)$st->fetchColumn() > 0) { return ['ok' => false, 'error' => '已经有叫「' . $name . '」的分组了。']; } // ⚠️ 别写成 `$max = (int)$db->prepare(...)->execute([...])`: // PDOStatement::execute() 返回 bool,强转永远是 1 —— 位置会算错。 $st2 = Database::get()->prepare('SELECT COALESCE(MAX(position), 0) FROM person_groups WHERE family_id = ?'); $st2->execute([$familyId]); $max = (int)$st2->fetchColumn(); Database::get()->prepare( 'INSERT INTO person_groups (family_id, name, position, is_builtin) VALUES (?, ?, ?, 0)' )->execute([$familyId, $name, max($max + 10, 80)]); return ['ok' => true, 'id' => (int)Database::get()->lastInsertId()]; } /** @return array{ok:bool,error?:string} */ public static function rename(int $familyId, int $id, string $name): array { $name = trim($name); $bad = self::validateName($name); if ($bad !== '') { return ['ok' => false, 'error' => $bad]; } $st = Database::get()->prepare( 'SELECT COUNT(*) FROM person_groups WHERE family_id = ? AND name = ? AND id <> ?' ); $st->execute([$familyId, $name, $id]); if ((int)$st->fetchColumn() > 0) { return ['ok' => false, 'error' => '已经有叫「' . $name . '」的分组了。']; } $st = Database::get()->prepare('UPDATE person_groups SET name = ? WHERE family_id = ? AND id = ?'); $st->execute([$name, $familyId, $id]); if ($st->rowCount() === 0) { return ['ok' => false, 'error' => '找不到这个分组。']; } return ['ok' => true]; } /** * 删除一个分组。挂在这个分组下的人**不会消失**,只是变成「未分类」—— * 名册上会多出一个「未分类」组提醒去归类。 * 最后一个分组不给删:全删光就没有任何分类可用了,那是死路。 * * @return array{ok:bool,error?:string,moved?:int} */ public static function delete(int $familyId, int $id): array { $all = self::byId($familyId); if (!isset($all[$id])) { return ['ok' => false, 'error' => '找不到这个分组。']; } if (count($all) <= 1) { return ['ok' => false, 'error' => '至少要留一个分组 —— 全删光就没法给成员归类了。']; } $db = Database::get(); $st = $db->prepare('SELECT COUNT(*) FROM persons WHERE family_id = ? AND group_id = ?'); $st->execute([$familyId, $id]); $moved = (int)$st->fetchColumn(); $db->prepare('UPDATE persons SET group_id = NULL WHERE family_id = ? AND group_id = ?') ->execute([$familyId, $id]); $db->prepare('DELETE FROM person_groups WHERE family_id = ? AND id = ?') ->execute([$familyId, $id]); // 各人自定义顺序里可能留着这个 id,清掉免得越积越脏 self::pruneOrder($familyId, $id); return ['ok' => true, 'moved' => $moved]; } /** 分组名规则:1–20 字、不许只剩空白 */ private static function validateName(string $name): string { if ($name === '') { return '分组名不能为空。'; } if (mb_strlen($name, 'UTF-8') > 20) { return '分组名请控制在 20 字以内。'; } return ''; } /** 从所有人的自定义顺序里剔除某个分组 id(删组之后调用) */ private static function pruneOrder(int $familyId, int $removedId): void { $db = Database::get(); $st = $db->prepare( 'SELECT us.person_id, us.v FROM user_settings us JOIN persons p ON p.id = us.person_id WHERE p.family_id = ? AND us.k = ?' ); $st->execute([$familyId, 'group_order']); foreach ($st->fetchAll() as $row) { $ids = json_decode((string)$row['v'], true); if (!is_array($ids)) { continue; } $keep = array_values(array_filter($ids, fn($x) => (int)$x !== $removedId)); $db->prepare('UPDATE user_settings SET v = ? WHERE person_id = ? AND k = ?') ->execute([json_encode(array_map('intval', $keep)), (int)$row['person_id'], 'group_order']); } } /** * 老库升级:把还没有分组的人按名册类型补一个合理默认。 * 家庭成员 → 家人;外部联系人 → 认识。 * 只补 NULL,用户自己改过的一律不动(幂等)。 */ public static function backfillMissing(\PDO $db): void { foreach ($db->query('SELECT id FROM families')->fetchAll() as $fam) { $fid = (int)$fam['id']; // 家庭可能还没套用预置分组(ensure 的 seed 在表建好之后才跑) $st = $db->prepare('SELECT COUNT(*) FROM person_groups WHERE family_id = ?'); $st->execute([$fid]); if ((int)$st->fetchColumn() === 0) { self::seedDefaults($fid); } foreach ([['member', '家人'], ['contact', '认识']] as [$kind, $name]) { $gid = self::idByName($fid, $name); if ($gid === 0) { continue; } $db->prepare( 'UPDATE persons SET group_id = ? WHERE family_id = ? AND kind = ? AND group_id IS NULL' )->execute([$gid, $fid, $kind]); } } } /** 按名字找分组 id(找不到返回 0) */ public static function idByName(int $familyId, string $name): int { $st = Database::get()->prepare('SELECT id FROM person_groups WHERE family_id = ? AND name = ?'); $st->execute([$familyId, $name]); return (int)($st->fetchColumn() ?: 0); } /** 每个分组里有多少人(没分组的人记在键 0 上) */ public static function counts(int $familyId, string $kind = 'member'): array { $st = Database::get()->prepare( 'SELECT COALESCE(group_id, 0) AS g, COUNT(*) AS n FROM persons WHERE family_id = ? AND kind = ? GROUP BY COALESCE(group_id, 0)' ); $st->execute([$familyId, $kind]); $out = []; foreach ($st->fetchAll() as $r) { $out[(int)$r['g']] = (int)$r['n']; } return $out; } }