'已经装好了', 'message' => '「家书」已经安装过。如果要重新安装,请先清空数据库里的 families / persons / accounts / settings 四张表。', ]); exit; } $dbName = $GLOBALS['config']['db']['name']; if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') { csrf_check(); $familyName = trim((string)($_POST['family_name'] ?? '')); $motto = trim((string)($_POST['motto'] ?? '')); $name = trim((string)($_POST['name'] ?? '')); $phone = trim((string)($_POST['phone'] ?? '')); $password = (string)($_POST['password'] ?? ''); $errors = []; if ($familyName === '') { $errors['_'] = '家庭名称不能为空'; } if ($name === '') { $errors['name'] = '请填写你的姓名'; } if (!preg_match('/^\+?[0-9\-]{6,20}$/', $phone)) { $errors['phone'] = '手机号格式看起来不对'; } $pwErr = Account::validatePassword($password); if ($pwErr) { $errors['password'] = implode(';', $pwErr); } if (!$errors) { try { // ensure() 而不是逐条 exec(statements()):它还会补齐后加的列与枚举值, // 所以老库重新跑一次就能自动升级,不必手工改表。 Schema::ensure(); $familyId = Family::create($familyName, $motto); // 关系分组的预置值:在这里建好,免得「装完第一眼进名册发现一个分组都没有」 // (ensure() 里也会补,但那要等下一次请求) \App\PersonGroup::seedDefaults((int)$familyId); // 装完就把结构版本盖上:不盖的话,第一次进首页发现"版本对不上", // 会再跑一次 ensure()(幂等,不出错,但白跑一遍 ALTER 查询)。 // ⚠️ 指纹也要在这里记下 —— 它是"要不要跑结构升级"的真正判据 // (见 Schema::fingerprint())。只写 schema_version 的话, // 装完的第一个请求会因为"指纹为空"而白跑一遍完整升级 // (幂等、不会错,但没必要,而且会让"升级跑过了吗"这个信号变模糊)。 \App\Schema::markUpgraded($familyId); $personId = Person::create($familyId, [ 'name' => $name, 'gender' => '', 'avatar' => '', 'phone' => $phone, 'id_card' => '', 'email' => '', 'address' => '', 'note' => '', 'slogan' => '', 'kind' => 'member', ]); Account::create($personId, $password, 'admin'); // 名册是「每人一份」(v5 起)。创始人先把自己放进自己的名册 —— // 之后他每加一个人,那个人就自动进他的名册(见 index.php 的 people/create)。 // ⚠️ 这里**不能**把全家成员都塞给他:新装时除他之外还没有别人, // 而"别人"要靠他自己加或复用,这正是"每人一份"的意思。 \App\PersonRoster::ensure((int)$familyId, (int)$personId, (int)$personId, null, 'self'); // 默认日历本(观影 / 旅行):**必须在这里补一遍**,因为上面那次 // Schema::ensure() 跑的时候这个人还没建出来(ensure 里的补建遍历的是 // "已开通账号的人")—— 只在 ensure 里做的话,装完的这个人一本日历都没有, // 而且版本号已被盖上,升级路径也不会再来补。 // (登录处还有第二道兜底,见 Auth::login。) \App\Calendar::seedDefaults((int)$familyId, (int)$personId); // 装完就删掉自己 —— 避免把安装入口长期暴露在公网 @unlink(__FILE__); header('Location: index.php?r=auth/login'); exit; } catch (\Throwable $ex) { $errors['_'] = '安装失败:' . $ex->getMessage(); } } set_old($_POST); View::renderBare('install', [ 'title' => '安装家书', 'dbName' => $dbName, 'errors' => $errors, ]); exit; } clear_old(); View::renderBare('install', [ 'title' => '安装家书', 'dbName' => $dbName, 'errors' => [], ]);