1. 独立页(登录/安装/错误)套上 HTML 骨架 —— 修「首页为什么没有样式」: 新增 templates/layout-bare.php,View::render 对独立页也走完整 <head> + CSS。 2. 以个人为中心:Kinship::label() 现算称呼、Auth::homeTitle() 出「X的家书」; 同一批人在不同登录者眼里自动变成公公婆婆 / 岳父岳母(不存一次性称谓)。 3. 附件存储可配置:Storage 门面(local / 阿里云 OSS),库里只存相对键, 云端失败自动回退本地;设置页可选驱动并填 OSS 参数(照 MovieLog 的方案)。 4. 三种角色:admin / member / guest,访客只读、写操作一律 403; accounts.role 枚举扩容,老库靠 Schema::ensure() 自动升级。 5. 家谱:只存相邻两代的边(parent / spouse),其余称呼全部推导 —— 兄弟姐妹不存边(共同父母 + birth_rank 排行推长子/次女、哥哥/妹妹), 认不清的人用虚拟人物 is_placeholder 占位,关系天然双向。 顺带修掉本地 e2e 挖出的 8 个线上级 bug(性别写死、长幼方向反、关系方向静默写反、 birth_rank 不从表单取回、隐式可空参数致响应头失效、无文件字段被误判为上传失败、 layout 变量撞车覆盖 $me、世代 0 显示成「下 0 代」)。 验证:kinship 单测 37/0 · HTTP e2e 81/0 · 老库升级专测 14/0 · 反向验证 11/11(把每个修复改回旧写法,对应断言都会变红)。
98 lines
3.1 KiB
PHP
98 lines
3.1 KiB
PHP
<?php
|
||
/**
|
||
* 安装向导:建表 → 建家庭 → 建第一个人(同时是他/她的管理员账号)。
|
||
*
|
||
* 「是否已安装」以**数据库里的四张表**为准,不以某个文件是否存在为准 ——
|
||
* 文件可能被删、可能被恢复,表在就是装过了。
|
||
*/
|
||
|
||
declare(strict_types=1);
|
||
|
||
require __DIR__ . '/app/bootstrap.php';
|
||
|
||
use App\Account;
|
||
use App\Crypto;
|
||
use App\Database;
|
||
use App\Family;
|
||
use App\Person;
|
||
use App\Schema;
|
||
use App\View;
|
||
|
||
if (Database::hasTables()) {
|
||
// 装过之后这个入口就该消失:给 404,别让它在公网一直挂着、也别让爬虫收录
|
||
http_response_code(404);
|
||
View::renderBare('error', [
|
||
'title' => '已经装好了',
|
||
'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);
|
||
$personId = Person::create($familyId, [
|
||
'name' => $name,
|
||
'nickname' => '',
|
||
'gender' => '',
|
||
'birthday' => '',
|
||
'avatar' => '',
|
||
'phone' => $phone,
|
||
'id_card' => '',
|
||
'email' => '',
|
||
'address' => '',
|
||
'note' => '',
|
||
'sort_order'=> 1,
|
||
'kind' => 'member',
|
||
]);
|
||
Account::create($personId, $password, 'admin');
|
||
|
||
// 装完就删掉自己 —— 避免把安装入口长期暴露在公网
|
||
@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' => [],
|
||
]);
|