exec($sql); } self::addColumn($db, 'persons', 'birth_rank', "ADD COLUMN `birth_rank` INT DEFAULT NULL COMMENT '家中排行(同一对父母的子女里,1=老大),用来算长子/次女' AFTER `gender`"); self::addColumn($db, 'persons', 'is_placeholder', "ADD COLUMN `is_placeholder` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '虚拟人物:还没弄清真实身份的占位(如「我的父亲」)' AFTER `note`"); self::widenRoleEnum($db); // ---- 三个生日(版本 3)---- // birthday 原本是 DATE,现在要精确到时分秒 → 升成 DATETIME。 // MODIFY 是幂等的(已经是 DATETIME 时再 MODIFY 也不报错、不丢数据)。 $db->exec("ALTER TABLE `persons` MODIFY COLUMN `birthday` DATETIME DEFAULT NULL COMMENT '真实阳历生日(精确到时分秒)'"); self::addColumn($db, 'persons', 'birthday_id_card', "ADD COLUMN `birthday_id_card` DATE DEFAULT NULL COMMENT '身份证上的生日:由身份证第 7-14 位推导,只读、永不变' AFTER `birthday`"); self::addColumn($db, 'persons', 'birthday_lunar', "ADD COLUMN `birthday_lunar` DATETIME DEFAULT NULL COMMENT '农历生日:**日期部分是农历年月日**,时间部分与阳历一致' AFTER `birthday_id_card`"); self::addColumn($db, 'persons', 'birthday_lunar_leap', "ADD COLUMN `birthday_lunar_leap` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'birthday_lunar 的月份是否为闰月' AFTER `birthday_lunar`"); self::addColumn($db, 'persons', 'slogan', "ADD COLUMN `slogan` VARCHAR(200) DEFAULT NULL COMMENT '个人的一句话;没填就用家庭统一的那句' AFTER `note`"); // 老数据里 birthday 若是 DATE,升成 DATETIME 后时间部分自动是 00:00:00,无需回填。 // 把老数据补出农历生日:只在还没算过时算一次(幂等)。 self::backfillLunarBirthday($db); self::backfillIdCardBirthday($db); // ---- 称呼从「人的属性」改成「视角属性」---- // 老语义:persons.nickname 是全家共用的小名。 // 新语义:nicknames 表,(viewer, target) → 我叫他什么。 // ⚠️ 必须在这里搬:不然升级之后界面上再没有人读 persons.nickname, // 用户会觉得"以前填的小名一夜之间全没了"。搬的时候给每个账号都写一份, // 迁移前后大家看到的叫法完全一致(`Nickname::migrateLegacy` 里说明了原因)。 // 放在结构升级里、而不是"某次请求里顺手做",是为了升级完就已经搬好。 foreach ($db->query('SELECT id FROM families')->fetchAll() as $fam) { \App\Nickname::migrateLegacy((int)$fam['id']); } } /** 老库已有阳历生日但没有农历生日 → 补算一次。用「为空」作条件,天然幂等。 */ private static function backfillLunarBirthday(\PDO $db): void { $st = $db->query( "SELECT id, birthday FROM persons WHERE birthday IS NOT NULL AND (birthday_lunar IS NULL OR birthday_lunar = '')" ); $rows = $st->fetchAll(); if (!$rows) { return; } $up = $db->prepare('UPDATE persons SET birthday_lunar = ?, birthday_lunar_leap = ? WHERE id = ?'); foreach ($rows as $r) { $bd = substr((string)$r['birthday'], 0, 10); [$y, $m, $d] = array_map('intval', explode('-', $bd)); $lu = \App\Lunar::solarToLunar($y, $m, $d); if ($lu === null) { continue; } $up->execute([ sprintf('%04d-%02d-%02d %s', $lu['year'], $lu['month'], $lu['day'], substr((string)$r['birthday'], 11, 8) ?: '00:00:00'), $lu['is_leap'] ? 1 : 0, (int)$r['id'], ]); } } /** 老库已有身份证号但没有「身份证生日」→ 推导一次。同样以「为空」为条件。 */ private static function backfillIdCardBirthday(\PDO $db): void { $st = $db->query( "SELECT id, id_card_encrypted FROM persons WHERE id_card_encrypted IS NOT NULL AND id_card_encrypted <> '' AND (birthday_id_card IS NULL)" ); $rows = $st->fetchAll(); if (!$rows) { return; } $up = $db->prepare('UPDATE persons SET birthday_id_card = ? WHERE id = ?'); foreach ($rows as $r) { try { $id = \App\Crypto::decrypt((string)$r['id_card_encrypted']); } catch (\Throwable $ex) { continue; // 密钥换过 / 解不开就跳过,绝不能因此让整次升级失败 } $bd = \App\Birthday::fromIdCard($id); if ($bd !== null) { $up->execute([$bd, (int)$r['id']]); } } } /** 列不存在才加 */ private static function addColumn(\PDO $db, string $table, string $column, string $alter): void { $st = $db->prepare( 'SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?' ); $st->execute([$table, $column]); if ((int)$st->fetchColumn() === 0) { $db->exec("ALTER TABLE `{$table}` {$alter}"); } } /** accounts.role 原本只有 admin/member,升级时要把 guest 加进枚举 */ private static function widenRoleEnum(\PDO $db): void { $st = $db->prepare( "SELECT COLUMN_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'accounts' AND COLUMN_NAME = 'role'" ); $st->execute(); $type = (string)$st->fetchColumn(); if ($type !== '' && strpos($type, 'guest') === false) { $db->exec( "ALTER TABLE `accounts` MODIFY COLUMN `role` ENUM('admin','member','guest') NOT NULL DEFAULT 'member' COMMENT '管理员 / 家庭成员 / 访客'" ); } } }