prepare( 'SELECT nickname FROM nicknames WHERE viewer_id = ? AND target_id = ? LIMIT 1' ); $st->execute([$viewerId, $targetId]); $v = $st->fetchColumn(); return $v === false ? '' : (string)$v; } /** 设置(空串 = 删掉这条,恢复成用姓名) */ public static function set(int $familyId, int $viewerId, int $targetId, string $nick): void { if ($viewerId <= 0 || $targetId <= 0 || $viewerId === $targetId) { return; // 自己不给自己起称呼 } $nick = trim(preg_replace('/\s+/u', ' ', $nick)); $db = Database::get(); if ($nick === '') { $db->prepare('DELETE FROM nicknames WHERE viewer_id = ? AND target_id = ?') ->execute([$viewerId, $targetId]); return; } if (mb_strlen($nick, 'UTF-8') > self::MAX_LEN) { $nick = mb_substr($nick, 0, self::MAX_LEN, 'UTF-8'); } $db->prepare( 'INSERT INTO nicknames (family_id, viewer_id, target_id, nickname) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE nickname = VALUES(nickname)' )->execute([$familyId, $viewerId, $targetId, $nick]); } /** 我设过的全部称呼:target_id => nickname */ public static function mapFor(int $viewerId): array { if ($viewerId <= 0) { return []; } $st = Database::get()->prepare('SELECT target_id, nickname FROM nicknames WHERE viewer_id = ?'); $st->execute([$viewerId]); $out = []; foreach ($st->fetchAll() as $r) { $out[(int)$r['target_id']] = (string)$r['nickname']; } return $out; } /** * 显示名:有「我怎么称呼他」就用它,否则用姓名。 * 页面上凡是"给人看的人名"都该走这里,而不是直接 echo name。 */ public static function display(int $viewerId, ?array $person): string { if (!$person) { return ''; } $nick = self::get($viewerId, (int)($person['id'] ?? 0)); return $nick !== '' ? $nick : (string)($person['name'] ?? ''); } /** * 一次性把老库里的 persons.nickname 搬进来。 * * 老语义是"全家共用一个小名",新语义是"每人各自称呼"。 * 迁移时**给家里每个账号都写一份**,这样迁移前后大家看到的叫法完全一样; * 之后谁想改成自己的叫法,改就是了。 * 幂等:靠 settings 里的标记,搬过一次就不再搬(否则会把用户后来的修改冲掉)。 */ public static function migrateLegacy(int $familyId): int { if ((string)Setting::get($familyId, 'nickname_migrated', '0') === '1') { return 0; } $db = Database::get(); // 旧列已经不在了(将来的清理,或全新安装)→ 没有任何东西可搬,直接记成已搬。 // 这一步让迁移"以后删掉那列也不会炸",否则升级会在信息模式查询上直接抛异常。 $has = (int)$db->query( "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'persons' AND COLUMN_NAME = 'nickname'" )->fetchColumn(); if ($has === 0) { Setting::set($familyId, 'nickname_migrated', '1'); return 0; } $rows = $db->prepare( "SELECT id, nickname FROM persons WHERE family_id = ? AND nickname IS NOT NULL AND nickname <> ''" ); $rows->execute([$familyId]); $legacy = $rows->fetchAll(); $viewers = $db->prepare( "SELECT p.id FROM persons p JOIN accounts a ON a.person_id = p.id WHERE p.family_id = ? AND a.status = 'active'" ); $viewers->execute([$familyId]); $viewerIds = array_map('intval', array_column($viewers->fetchAll(), 'id')); $ins = $db->prepare( 'INSERT IGNORE INTO nicknames (family_id, viewer_id, target_id, nickname) VALUES (?,?,?,?)' ); $n = 0; foreach ($legacy as $p) { foreach ($viewerIds as $vid) { if ($vid === (int)$p['id']) { continue; // 自己不给自己起称呼 } $ins->execute([$familyId, $vid, (int)$p['id'], (string)$p['nickname']]); $n++; } } Setting::set($familyId, 'nickname_migrated', '1'); return $n; } /** 我给别人起的全部称呼(个人信息页用),带被称呼人的姓名 */ public static function mineWithNames(int $familyId, int $viewerId): array { $st = Database::get()->prepare( 'SELECT n.target_id, n.nickname, p.name, p.avatar, p.is_placeholder FROM nicknames n JOIN persons p ON p.id = n.target_id WHERE n.viewer_id = ? AND n.family_id = ? ORDER BY p.sort_order ASC, p.id ASC' ); $st->execute([$viewerId, $familyId]); return $st->fetchAll(); } }