五项家庭化改造:以个人为中心 · 家谱 · 三种角色 · 附件存储可配置 · 独立页样式
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(把每个修复改回旧写法,对应断言都会变红)。
This commit is contained in:
@@ -15,7 +15,10 @@ use App\Auth;
|
||||
use App\Crypto;
|
||||
use App\Database;
|
||||
use App\Family;
|
||||
use App\Kinship;
|
||||
use App\Person;
|
||||
use App\Relations;
|
||||
use App\Schema;
|
||||
use App\Setting;
|
||||
use App\Upload;
|
||||
use App\View;
|
||||
@@ -25,6 +28,40 @@ if (!Database::hasTables()) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一次性结构升级。
|
||||
*
|
||||
* ⚠️ 为什么要有这段:上线后的库不认识新加的列(birth_rank / is_placeholder)和
|
||||
* 新的 guest 角色。靠人去手动 ALTER 迟早会漏,所以在每次请求的开头比一下版本号,
|
||||
* 落后了就补一次 —— 平时只是一次字符串比较,几乎零成本。
|
||||
*/
|
||||
const FB_SCHEMA_VERSION = '2';
|
||||
if (Database::hasTables()) {
|
||||
$familyIdForSchema = isset($_SESSION['family_id']) ? (int)$_SESSION['family_id'] : 0;
|
||||
// 登录前还没有 family_id,用首个家庭兜底(本系统是单家庭部署)
|
||||
if ($familyIdForSchema === 0) {
|
||||
try {
|
||||
$familyIdForSchema = (int)(Database::get()->query('SELECT MIN(id) FROM families')->fetchColumn() ?: 0);
|
||||
} catch (\Throwable $ex) {
|
||||
$familyIdForSchema = 0;
|
||||
}
|
||||
}
|
||||
if ($familyIdForSchema > 0) {
|
||||
$have = (string)Setting::get($familyIdForSchema, 'schema_version', '1');
|
||||
if ($have !== FB_SCHEMA_VERSION) {
|
||||
try {
|
||||
Schema::ensure();
|
||||
Setting::set($familyIdForSchema, 'schema_version', FB_SCHEMA_VERSION);
|
||||
} catch (\Throwable $ex) {
|
||||
// 升级失败不能把整站挡住;下次请求会再试
|
||||
if (!empty($GLOBALS['config']['debug'])) {
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$r = isset($_GET['r']) ? trim((string)$_GET['r'], '/') : 'home';
|
||||
$r = $r === '' ? 'home' : $r;
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
@@ -101,29 +138,88 @@ switch ($r) {
|
||||
View::render('auth/password', ['title' => '修改密码'], 'home');
|
||||
break;
|
||||
|
||||
// ==================== 首页 ====================
|
||||
// ==================== 首页(以我为中心) ====================
|
||||
case 'home':
|
||||
$familyId = (int)Auth::familyId();
|
||||
$me = (int)Auth::personId();
|
||||
$people = Person::map($familyId);
|
||||
$graph = Kinship::graph($familyId);
|
||||
|
||||
// 「我的家人」= 以我为中心上下各三代那张家谱里的人。
|
||||
// ⚠️ 早先只取「直接关系」(父母/配偶/子女/兄弟姐妹),于是**姻亲与祖孙都被漏掉**:
|
||||
// 爱人登进来看不到「公公婆婆」,我看不到「岳父岳母」,爷爷奶奶也不在首页上。
|
||||
// 这恰恰是「以个人为中心」最该体现的地方,所以改用家谱的节点集合。
|
||||
$tree = Kinship::tree($graph, $me, $people, 3, 3);
|
||||
$kinIds = array_diff(array_map('intval', array_keys($tree['nodes'])), [$me]);
|
||||
$kin = [];
|
||||
// 用 Person::all()(带账号信息)而不是 Person::map(),首页才能显示管理员/访客徽标
|
||||
foreach (Person::all($familyId) as $p) {
|
||||
$pid = (int)$p['id'];
|
||||
if (in_array($pid, $kinIds, true)) {
|
||||
$kin[$pid] = $p + ['label' => Kinship::label($graph, $me, $pid, $people)];
|
||||
}
|
||||
}
|
||||
$birthdays = Person::upcomingBirthdays($familyId);
|
||||
// 生日条上也标出称谓
|
||||
foreach ($birthdays as &$b) {
|
||||
$b['label'] = isset($people[(int)$b['id']])
|
||||
? Kinship::label($graph, $me, (int)$b['id'], $people) : '';
|
||||
}
|
||||
unset($b);
|
||||
|
||||
View::render('home', [
|
||||
'title' => '首页',
|
||||
'family' => Family::find($familyId),
|
||||
'people' => Person::all($familyId),
|
||||
'birthdays' => Person::upcomingBirthdays($familyId),
|
||||
'title' => '首页',
|
||||
'family' => Family::find($familyId),
|
||||
'me' => $people[$me] ?? null,
|
||||
'meId' => $me,
|
||||
'kin' => $kin,
|
||||
'allCount' => count($people),
|
||||
'relCount' => Relations::count($familyId),
|
||||
'birthdays' => $birthdays,
|
||||
], 'home');
|
||||
break;
|
||||
|
||||
// ==================== 家谱(以我为中心) ====================
|
||||
case 'kin':
|
||||
$familyId = (int)Auth::familyId();
|
||||
$me = (int)Auth::personId();
|
||||
$people = Person::map($familyId);
|
||||
$graph = Kinship::graph($familyId);
|
||||
$tree = Kinship::tree($graph, $me, $people, 3, 3);
|
||||
|
||||
View::render('kin/tree', [
|
||||
'title' => '我的家谱',
|
||||
'tree' => $tree,
|
||||
'meId' => $me,
|
||||
'people' => $people,
|
||||
'graph' => $graph,
|
||||
], 'kin');
|
||||
break;
|
||||
|
||||
// ==================== 名册 ====================
|
||||
case 'people':
|
||||
$q = trim((string)($_GET['q'] ?? ''));
|
||||
$familyId = (int)Auth::familyId();
|
||||
$me = (int)Auth::personId();
|
||||
$rows = Person::all($familyId, $q);
|
||||
$people = Person::map($familyId);
|
||||
$graph = Kinship::graph($familyId);
|
||||
// 名册里也标出「我该怎么称呼他」
|
||||
foreach ($rows as &$row) {
|
||||
$row['label'] = Kinship::label($graph, $me, (int)$row['id'], $people);
|
||||
}
|
||||
unset($row);
|
||||
|
||||
View::render('people/index', [
|
||||
'title' => '家庭名册',
|
||||
'people' => Person::all((int)Auth::familyId(), $q),
|
||||
'people' => $rows,
|
||||
'q' => $q,
|
||||
'meId' => $me,
|
||||
], 'people');
|
||||
break;
|
||||
|
||||
case 'people/new':
|
||||
Auth::requireAdmin();
|
||||
Auth::requireWrite();
|
||||
clear_old();
|
||||
View::render('people/form', [
|
||||
'title' => '添加成员',
|
||||
@@ -133,7 +229,7 @@ switch ($r) {
|
||||
break;
|
||||
|
||||
case 'people/create':
|
||||
Auth::requireAdmin();
|
||||
Auth::requireWrite();
|
||||
csrf_check();
|
||||
$d = [
|
||||
'name' => trim((string)($_POST['name'] ?? '')),
|
||||
@@ -148,10 +244,14 @@ switch ($r) {
|
||||
'sort_order'=> (int)($_POST['sort_order'] ?? 1000),
|
||||
'avatar' => '',
|
||||
'kind' => 'member',
|
||||
// ⚠️ 这两项必须显式从表单取回:表单里有,但漏了这一步就会被静默丢掉,
|
||||
// 表现是「排行填了没用、虚拟人物标记不生效」,且不会有任何报错。
|
||||
'birth_rank' => trim((string)($_POST['birth_rank'] ?? '')),
|
||||
'is_placeholder' => !empty($_POST['is_placeholder']) ? 1 : 0,
|
||||
];
|
||||
$errors = Person::validate($d);
|
||||
|
||||
$up = Upload::avatar($_FILES['avatar'] ?? []);
|
||||
$up = Upload::avatar((int)Auth::familyId(), $_FILES['avatar'] ?? []);
|
||||
if (!$up['ok'] && ($up['error'] ?? '') !== '') {
|
||||
$errors['avatar'] = $up['error'];
|
||||
}
|
||||
@@ -188,7 +288,7 @@ switch ($r) {
|
||||
break;
|
||||
|
||||
case 'people/edit':
|
||||
Auth::requireAdmin();
|
||||
Auth::requireWrite();
|
||||
$p = Person::find((int)($_GET['id'] ?? 0));
|
||||
if (!$p || (int)$p['family_id'] !== (int)Auth::familyId()) {
|
||||
flash('err', '找不到这个人。');
|
||||
@@ -205,7 +305,7 @@ switch ($r) {
|
||||
break;
|
||||
|
||||
case 'people/update':
|
||||
Auth::requireAdmin();
|
||||
Auth::requireWrite();
|
||||
csrf_check();
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$p = Person::find($id);
|
||||
@@ -225,9 +325,11 @@ switch ($r) {
|
||||
'note' => trim((string)($_POST['note'] ?? '')),
|
||||
'sort_order'=> (int)($_POST['sort_order'] ?? 1000),
|
||||
'avatar' => (string)$p['avatar'],
|
||||
'birth_rank' => trim((string)($_POST['birth_rank'] ?? '')),
|
||||
'is_placeholder' => !empty($_POST['is_placeholder']) ? 1 : 0,
|
||||
];
|
||||
$errors = Person::validate($d);
|
||||
$up = Upload::avatar($_FILES['avatar'] ?? []);
|
||||
$up = Upload::avatar((int)Auth::familyId(), $_FILES['avatar'] ?? []);
|
||||
if (!$up['ok'] && ($up['error'] ?? '') !== '') {
|
||||
$errors['avatar'] = $up['error'];
|
||||
}
|
||||
@@ -243,7 +345,7 @@ switch ($r) {
|
||||
exit;
|
||||
}
|
||||
if ($up['ok']) {
|
||||
Upload::remove((string)$p['avatar']);
|
||||
Upload::remove((int)Auth::familyId(), (string)$p['avatar']);
|
||||
$d['avatar'] = $up['file'];
|
||||
}
|
||||
try {
|
||||
@@ -268,14 +370,33 @@ switch ($r) {
|
||||
break;
|
||||
|
||||
case 'people/show':
|
||||
$p = Person::findWithAccount((int)($_GET['id'] ?? 0));
|
||||
if (!$p || (int)$p['family_id'] !== (int)Auth::familyId()) {
|
||||
$familyId = (int)Auth::familyId();
|
||||
$me = (int)Auth::personId();
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$p = Person::findWithAccount($id);
|
||||
if (!$p || (int)$p['family_id'] !== $familyId) {
|
||||
flash('err', '找不到这个人。');
|
||||
redirect('people');
|
||||
}
|
||||
$people = Person::map($familyId);
|
||||
$graph = Kinship::graph($familyId);
|
||||
|
||||
// 这个人在「我」眼里是谁,以及我与他之间的直接关系(可编辑的那些)
|
||||
$tree = Kinship::tree($graph, $id, $people, 2, 2);
|
||||
|
||||
View::render('people/show', [
|
||||
'title' => $p['name'],
|
||||
'person' => $p,
|
||||
'title' => $p['name'],
|
||||
'person' => $p,
|
||||
'meId' => $me,
|
||||
'people' => $people,
|
||||
'graph' => $graph,
|
||||
'labelToMe' => Kinship::label($graph, $me, $id, $people),
|
||||
'isMe' => $id === $me,
|
||||
// 以这个人为中心的两代亲属,供详情页展示
|
||||
'theirTree' => $tree,
|
||||
'theirRels' => Relations::forPerson($familyId, $id),
|
||||
'options' => Person::options($familyId, $id),
|
||||
'kinReady' => Kinship::graph($familyId) !== [],
|
||||
], 'people');
|
||||
break;
|
||||
|
||||
@@ -295,7 +416,7 @@ switch ($r) {
|
||||
flash('err', '这是唯一的管理员,先把另一个人设为管理员再移除。');
|
||||
redirect('people/show', ['id' => $id]);
|
||||
}
|
||||
Upload::remove((string)$p['avatar']);
|
||||
Upload::remove((int)Auth::familyId(), (string)$p['avatar']);
|
||||
Person::delete($id);
|
||||
flash('ok', '已从名册移除「' . $p['name'] . '」。');
|
||||
}
|
||||
@@ -326,7 +447,8 @@ switch ($r) {
|
||||
flash('err', implode(';', $errs));
|
||||
redirect('people/show', ['id' => $personId]);
|
||||
}
|
||||
Account::create($personId, $pass, ($_POST['role'] ?? '') === 'admin' ? 'admin' : 'member');
|
||||
$r0 = (string)($_POST['role'] ?? '');
|
||||
Account::create($personId, $pass, in_array($r0, Account::ROLES, true) ? $r0 : 'member');
|
||||
flash('ok', '账号已开通,用手机号就能登录。');
|
||||
redirect('people/show', ['id' => $personId]);
|
||||
break;
|
||||
@@ -335,7 +457,7 @@ switch ($r) {
|
||||
Auth::requireAdmin();
|
||||
csrf_check();
|
||||
$personId = (int)($_POST['person_id'] ?? 0);
|
||||
$role = ($_POST['role'] ?? '') === 'admin' ? 'admin' : 'member';
|
||||
$role = in_array(($r1 = (string)($_POST['role'] ?? '')), Account::ROLES, true) ? $r1 : 'member';
|
||||
$p = Person::find($personId);
|
||||
$acc = Account::findByPerson($personId);
|
||||
if ($p && $acc) {
|
||||
@@ -370,6 +492,109 @@ switch ($r) {
|
||||
redirect('people/show', ['id' => $personId]);
|
||||
break;
|
||||
|
||||
// ==================== 亲属关系 ====================
|
||||
case 'kin/relate':
|
||||
Auth::requireWrite();
|
||||
csrf_check();
|
||||
$familyId = (int)Auth::familyId();
|
||||
$from = (int)($_POST['from_id'] ?? 0); // 我(或当前正在编辑的人)
|
||||
$to = (int)($_POST['to_id'] ?? 0); // 对方
|
||||
$kind = (string)($_POST['kind'] ?? '');
|
||||
$role = (string)($_POST['role'] ?? '');
|
||||
|
||||
$ok = Person::find($from) && Person::find($to)
|
||||
&& (int)Person::find($from)['family_id'] === $familyId
|
||||
&& (int)Person::find($to)['family_id'] === $familyId;
|
||||
if (!$ok) {
|
||||
flash('err', '选的人不对。');
|
||||
redirect('people');
|
||||
}
|
||||
|
||||
// ⚠️⚠️ 方向极易写反,而且写反了**不会报任何错**:页面照常显示「关系已记录」,
|
||||
// 只是把人记成了对方的父母 —— 家谱整个倒过来。
|
||||
// 约定:表单里 from_id 是「当前打开的人」,也就是表单上写的那个「我」;
|
||||
// kind=parent 表示「对方是我的父母」→ 所以我是 child、对方是 parent。
|
||||
// Relations::addParent($familyId, $childId, $parentId, $role) 的第二个参数是**孩子**。
|
||||
if ($kind === 'parent') {
|
||||
// 对方是我的父母:我是孩子,对方是父母
|
||||
$r = Relations::addParent($familyId, $from, $to, $role);
|
||||
} elseif ($kind === 'child') {
|
||||
// 对方是我的子女:对方是孩子,我是父母
|
||||
$r = Relations::addParent($familyId, $to, $from, $role);
|
||||
} else {
|
||||
$r = Relations::addSpouse($familyId, $from, $to);
|
||||
}
|
||||
flash($r['ok'] ? 'ok' : 'err', $r['ok'] ? '关系已记录。' : $r['error']);
|
||||
redirect('people/show', ['id' => $from]);
|
||||
break;
|
||||
|
||||
case 'kin/unrelate':
|
||||
Auth::requireWrite();
|
||||
csrf_check();
|
||||
$familyId = (int)Auth::familyId();
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$who = (int)($_POST['person_id'] ?? 0);
|
||||
Relations::remove($familyId, $id);
|
||||
flash('ok', '这条关系已解除。');
|
||||
redirect('people/show', ['id' => $who]);
|
||||
break;
|
||||
|
||||
// ==================== 访客邀请 ====================
|
||||
case 'accounts/invite':
|
||||
Auth::requireAdmin();
|
||||
clear_old();
|
||||
View::render('people/invite', [
|
||||
'title' => '邀请访客',
|
||||
'people' => Person::all((int)Auth::familyId()),
|
||||
'options' => Person::options((int)Auth::familyId()),
|
||||
], 'people');
|
||||
break;
|
||||
|
||||
case 'accounts/invite_create':
|
||||
Auth::requireAdmin();
|
||||
csrf_check();
|
||||
$familyId = (int)Auth::familyId();
|
||||
$name = trim((string)($_POST['name'] ?? ''));
|
||||
$phone = trim((string)($_POST['phone'] ?? ''));
|
||||
$pass = (string)($_POST['password'] ?? '');
|
||||
$note = trim((string)($_POST['note'] ?? ''));
|
||||
|
||||
$errors = [];
|
||||
if ($name === '') {
|
||||
$errors['name'] = '请填写对方的名字';
|
||||
}
|
||||
if (!preg_match('/^\+?[0-9\-]{6,20}$/', $phone)) {
|
||||
$errors['phone'] = '手机号格式看起来不对';
|
||||
}
|
||||
$pw = Account::validatePassword($pass);
|
||||
if ($pw) {
|
||||
$errors['password'] = implode(';', $pw);
|
||||
}
|
||||
if (!$errors) {
|
||||
try {
|
||||
// 访客也进名册(kind=contact,与家庭成员区分开),但不参与家谱
|
||||
$pid = Person::create($familyId, [
|
||||
'name' => $name, 'nickname' => '', 'gender' => '', 'birthday' => '',
|
||||
'avatar' => '', 'phone' => $phone, 'id_card' => '', 'email' => '',
|
||||
'address' => '', 'note' => $note, 'sort_order' => 2000, 'kind' => 'contact',
|
||||
]);
|
||||
Account::create($pid, $pass, 'guest');
|
||||
clear_old();
|
||||
flash('ok', '访客「' . $name . '」已开通,把手机号和密码告诉他就能登录。');
|
||||
redirect('accounts/invite');
|
||||
} catch (\PDOException $ex) {
|
||||
$errors['phone'] = '这个手机号已经有人用了';
|
||||
}
|
||||
}
|
||||
set_old($_POST);
|
||||
View::render('people/invite', [
|
||||
'title' => '邀请访客',
|
||||
'people' => Person::all($familyId),
|
||||
'options' => Person::options($familyId),
|
||||
'errors' => $errors,
|
||||
], 'people');
|
||||
break;
|
||||
|
||||
// ==================== 设置 ====================
|
||||
case 'settings':
|
||||
Auth::requireAdmin();
|
||||
@@ -380,14 +605,77 @@ switch ($r) {
|
||||
// 拿不到版本不影响页面
|
||||
}
|
||||
$rawKey = (string)fb_config('app_key', '');
|
||||
$acc = Account::roleCounts();
|
||||
View::render('settings', [
|
||||
'title' => '设置',
|
||||
'family' => Family::find((int)Auth::familyId()),
|
||||
'dbVersion' => $dbVersion,
|
||||
'keyOk' => $rawKey !== '' && strpos($rawKey, '请填写') !== 0,
|
||||
'title' => '设置',
|
||||
'family' => Family::find((int)Auth::familyId()),
|
||||
'dbVersion' => $dbVersion,
|
||||
'keyOk' => $rawKey !== '' && strpos($rawKey, '请填写') !== 0,
|
||||
'roleCounts' => $acc,
|
||||
'relCount' => Relations::count((int)Auth::familyId()),
|
||||
'storageDriver' => \App\Storage\Storage::driver((int)Auth::familyId()),
|
||||
'storageOss' => \App\Storage\Storage::config((int)Auth::familyId()),
|
||||
'storageOk' => \App\Storage\Storage::configComplete((int)Auth::familyId()),
|
||||
'storageEffective' => \App\Storage\Storage::effectiveDriver((int)Auth::familyId()),
|
||||
], 'settings');
|
||||
break;
|
||||
|
||||
case 'settings/storage':
|
||||
Auth::requireAdmin();
|
||||
csrf_check();
|
||||
$familyId = (int)Auth::familyId();
|
||||
$old = \App\Storage\Storage::config($familyId);
|
||||
$in = (array)($_POST['oss'] ?? []);
|
||||
// 密钥留空 = 不改(避免「保存时没重填就把已存的密钥清掉」)
|
||||
foreach (['access_key_secret', 'access_key_id'] as $k) {
|
||||
if (trim((string)($in[$k] ?? '')) === '') {
|
||||
$in[$k] = (string)$old[$k];
|
||||
}
|
||||
}
|
||||
$oss = [
|
||||
'auth_mode' => ($in['auth_mode'] ?? 'ak') === 'ecs_role' ? 'ecs_role' : 'ak',
|
||||
'access_key_id' => trim((string)($in['access_key_id'] ?? '')),
|
||||
'access_key_secret' => trim((string)($in['access_key_secret'] ?? '')),
|
||||
'bucket' => trim((string)($in['bucket'] ?? '')),
|
||||
'endpoint' => trim((string)($in['endpoint'] ?? '')),
|
||||
'prefix' => trim((string)($in['prefix'] ?? '')),
|
||||
'ram_role_name' => trim((string)($in['ram_role_name'] ?? '')),
|
||||
'custom_domain' => trim((string)($in['custom_domain'] ?? '')),
|
||||
'internal_endpoint' => !empty($in['internal_endpoint']) ? '1' : '0',
|
||||
'https' => !empty($in['https']) ? '1' : '0',
|
||||
];
|
||||
\App\Storage\Storage::saveConfig($familyId, (string)($_POST['driver'] ?? 'local'), $oss);
|
||||
flash('ok', '附件存储设置已保存。');
|
||||
redirect('settings');
|
||||
break;
|
||||
|
||||
case 'settings/test_storage':
|
||||
Auth::requireAdmin();
|
||||
csrf_check();
|
||||
$familyId = (int)Auth::familyId();
|
||||
$disk = \App\Storage\Storage::disk($familyId);
|
||||
$err = $disk->validateConfig();
|
||||
if ($err !== '') {
|
||||
flash('err', '配置不完整:' . $err);
|
||||
redirect('settings');
|
||||
}
|
||||
// 真写一个探针对象再读回来,最后删掉 —— 比只 ping 一下可靠得多
|
||||
$probeKey = 'avatars/.probe-' . bin2hex(random_bytes(6)) . '.txt';
|
||||
$payload = 'familybook-probe-' . time();
|
||||
$put = \App\Storage\Storage::putContents($familyId, $payload, $probeKey, 'text/plain');
|
||||
if (empty($put['ok'])) {
|
||||
flash('err', '写入失败:' . (string)($put['error'] ?? ''));
|
||||
redirect('settings');
|
||||
}
|
||||
$get = \App\Storage\Storage::read($familyId, $probeKey);
|
||||
$ok = !empty($get['ok']) && trim((string)$get['body']) === $payload;
|
||||
\App\Storage\Storage::delete($familyId, $probeKey);
|
||||
flash($ok ? 'ok' : 'err',
|
||||
$ok ? '存储可用:写入 → 读回 → 删除 三步都通过了(驱动:' . $disk->id() . ')'
|
||||
: '写入成功但读回来对不上,请检查桶权限或内网/外网 endpoint。');
|
||||
redirect('settings');
|
||||
break;
|
||||
|
||||
case 'settings/update':
|
||||
Auth::requireAdmin();
|
||||
csrf_check();
|
||||
|
||||
Reference in New Issue
Block a user