'登录', 'familyName' => fb_config('site_name', '家书'), 'error' => '手机号或密码不对,或者账号还没开通/已被停用。', 'phone' => $phone, ]); exit; } View::renderBare('login', [ 'title' => '登录', 'familyName' => fb_config('site_name', '家书'), ]); break; case 'auth/logout': if (isset($_GET['_csrf'], $_SESSION['_csrf']) && hash_equals($_SESSION['_csrf'], (string)$_GET['_csrf'])) { Auth::logout(); } redirect('auth/login'); break; case 'auth/password': Auth::requireLogin(); if ($method === 'POST') { csrf_check(); $old = (string)($_POST['old_password'] ?? ''); $new = (string)($_POST['password'] ?? ''); $new2 = (string)($_POST['password2'] ?? ''); $st = Database::get()->prepare('SELECT password_hash FROM accounts WHERE id = ?'); $st->execute([Auth::id()]); $hash = (string)$st->fetchColumn(); $err = null; if (!password_verify($old, $hash)) { $err = '当前密码不对'; } elseif ($new !== $new2) { $err = '两次输入的新密码不一致'; } elseif (Account::validatePassword($new)) { $err = implode(';', Account::validatePassword($new)); } if ($err) { View::render('auth/password', ['title' => '修改密码', 'error' => $err], 'home'); exit; } Account::setPassword((int)Auth::id(), $new); flash('ok', '密码已更新,下次登录用新密码。'); redirect('home'); } View::render('auth/password', ['title' => '修改密码'], 'home'); break; // ==================== 首页 ==================== case 'home': $familyId = (int)Auth::familyId(); View::render('home', [ 'title' => '首页', 'family' => Family::find($familyId), 'people' => Person::all($familyId), 'birthdays' => Person::upcomingBirthdays($familyId), ], 'home'); break; // ==================== 名册 ==================== case 'people': $q = trim((string)($_GET['q'] ?? '')); View::render('people/index', [ 'title' => '家庭名册', 'people' => Person::all((int)Auth::familyId(), $q), 'q' => $q, ], 'people'); break; case 'people/new': Auth::requireAdmin(); clear_old(); View::render('people/form', [ 'title' => '添加成员', 'person' => null, 'errors' => [], ], 'people'); break; case 'people/create': Auth::requireAdmin(); csrf_check(); $d = [ 'name' => trim((string)($_POST['name'] ?? '')), 'nickname' => trim((string)($_POST['nickname'] ?? '')), 'gender' => (string)($_POST['gender'] ?? ''), 'birthday' => trim((string)($_POST['birthday'] ?? '')), 'phone' => trim((string)($_POST['phone'] ?? '')), 'id_card' => trim((string)($_POST['id_card'] ?? '')), 'email' => trim((string)($_POST['email'] ?? '')), 'address' => trim((string)($_POST['address'] ?? '')), 'note' => trim((string)($_POST['note'] ?? '')), 'sort_order'=> (int)($_POST['sort_order'] ?? 1000), 'avatar' => '', 'kind' => 'member', ]; $errors = Person::validate($d); $up = Upload::avatar($_FILES['avatar'] ?? []); if (!$up['ok'] && ($up['error'] ?? '') !== '') { $errors['avatar'] = $up['error']; } if ($errors) { set_old($_POST); View::render('people/form', [ 'title' => '添加成员', 'person' => null, 'errors' => $errors, 'phoneValue' => $d['phone'], 'idCardValue'=> $d['id_card'], ], 'people'); exit; } if ($up['ok']) { $d['avatar'] = $up['file']; } try { $id = Person::create((int)Auth::familyId(), $d); } catch (\PDOException $ex) { set_old($_POST); View::render('people/form', [ 'title' => '添加成员', 'person' => null, 'errors' => ['phone' => '这个手机号已经在名册里了'], 'phoneValue' => $d['phone'], 'idCardValue'=> $d['id_card'], ], 'people'); exit; } clear_old(); flash('ok', '已把「' . $d['name'] . '」加进名册。'); redirect('people/show', ['id' => $id]); break; case 'people/edit': Auth::requireAdmin(); $p = Person::find((int)($_GET['id'] ?? 0)); if (!$p || (int)$p['family_id'] !== (int)Auth::familyId()) { flash('err', '找不到这个人。'); redirect('people'); } clear_old(); View::render('people/form', [ 'title' => '编辑成员', 'person' => $p, 'errors' => [], 'phoneValue' => Crypto::decrypt((string)$p['phone_encrypted']), 'idCardValue' => Crypto::decrypt((string)$p['id_card_encrypted']), ], 'people'); break; case 'people/update': Auth::requireAdmin(); csrf_check(); $id = (int)($_POST['id'] ?? 0); $p = Person::find($id); if (!$p || (int)$p['family_id'] !== (int)Auth::familyId()) { flash('err', '找不到这个人。'); redirect('people'); } $d = [ 'name' => trim((string)($_POST['name'] ?? '')), 'nickname' => trim((string)($_POST['nickname'] ?? '')), 'gender' => (string)($_POST['gender'] ?? ''), 'birthday' => trim((string)($_POST['birthday'] ?? '')), 'phone' => trim((string)($_POST['phone'] ?? '')), 'id_card' => trim((string)($_POST['id_card'] ?? '')), 'email' => trim((string)($_POST['email'] ?? '')), 'address' => trim((string)($_POST['address'] ?? '')), 'note' => trim((string)($_POST['note'] ?? '')), 'sort_order'=> (int)($_POST['sort_order'] ?? 1000), 'avatar' => (string)$p['avatar'], ]; $errors = Person::validate($d); $up = Upload::avatar($_FILES['avatar'] ?? []); if (!$up['ok'] && ($up['error'] ?? '') !== '') { $errors['avatar'] = $up['error']; } if ($errors) { set_old($_POST); View::render('people/form', [ 'title' => '编辑成员', 'person' => $p, 'errors' => $errors, 'phoneValue' => $d['phone'], 'idCardValue' => $d['id_card'], ], 'people'); exit; } if ($up['ok']) { Upload::remove((string)$p['avatar']); $d['avatar'] = $up['file']; } try { Person::update($id, $d); } catch (\PDOException $ex) { set_old($_POST); View::render('people/form', [ 'title' => '编辑成员', 'person' => $p, 'errors' => ['phone' => '这个手机号已经被名册里的另一个人用了'], 'phoneValue' => $d['phone'], 'idCardValue' => $d['id_card'], ], 'people'); exit; } if ((int)$p['id'] === (int)Auth::personId()) { Auth::refreshAvatar((string)$d['avatar']); } clear_old(); flash('ok', '已保存。'); redirect('people/show', ['id' => $id]); break; case 'people/show': $p = Person::findWithAccount((int)($_GET['id'] ?? 0)); if (!$p || (int)$p['family_id'] !== (int)Auth::familyId()) { flash('err', '找不到这个人。'); redirect('people'); } View::render('people/show', [ 'title' => $p['name'], 'person' => $p, ], 'people'); break; case 'people/destroy': Auth::requireAdmin(); csrf_check(); $id = (int)($_POST['id'] ?? 0); $p = Person::find($id); if ($p && (int)$p['family_id'] === (int)Auth::familyId()) { if ((int)$p['id'] === (int)Auth::personId()) { flash('err', '不能把自己从名册里移除。'); redirect('people/show', ['id' => $id]); } // 不让最后一个管理员消失:系统里没人能管理家庭就锁死了 $acc = Account::findByPerson($id); if ($acc && $acc['role'] === 'admin' && Account::adminCount() <= 1) { flash('err', '这是唯一的管理员,先把另一个人设为管理员再移除。'); redirect('people/show', ['id' => $id]); } Upload::remove((string)$p['avatar']); Person::delete($id); flash('ok', '已从名册移除「' . $p['name'] . '」。'); } redirect('people'); break; // ==================== 账号 ==================== case 'accounts/create': Auth::requireAdmin(); csrf_check(); $personId = (int)($_POST['person_id'] ?? 0); $p = Person::find($personId); if (!$p || (int)$p['family_id'] !== (int)Auth::familyId()) { flash('err', '找不到这个人。'); redirect('people'); } if (Account::findByPerson($personId)) { flash('err', '这个人的账号已经开通过了。'); redirect('people/show', ['id' => $personId]); } if (Crypto::decrypt((string)$p['phone_encrypted']) === '') { flash('err', '要先填手机号,才能开通登录。'); redirect('people/show', ['id' => $personId]); } $pass = (string)($_POST['password'] ?? ''); $errs = Account::validatePassword($pass); if ($errs) { flash('err', implode(';', $errs)); redirect('people/show', ['id' => $personId]); } Account::create($personId, $pass, ($_POST['role'] ?? '') === 'admin' ? 'admin' : 'member'); flash('ok', '账号已开通,用手机号就能登录。'); redirect('people/show', ['id' => $personId]); break; case 'accounts/role': Auth::requireAdmin(); csrf_check(); $personId = (int)($_POST['person_id'] ?? 0); $role = ($_POST['role'] ?? '') === 'admin' ? 'admin' : 'member'; $p = Person::find($personId); $acc = Account::findByPerson($personId); if ($p && $acc) { if ($role === 'member' && $acc['role'] === 'admin' && Account::adminCount() <= 1) { flash('err', '至少要留一位管理员。'); redirect('people/show', ['id' => $personId]); } Account::setRole((int)$acc['id'], $role); flash('ok', '权限已更新。'); } redirect('people/show', ['id' => $personId]); break; case 'accounts/status': Auth::requireAdmin(); csrf_check(); $personId = (int)($_POST['person_id'] ?? 0); $status = ($_POST['status'] ?? '') === 'active' ? 'active' : 'disabled'; $acc = Account::findByPerson($personId); if ($acc) { if ((int)$acc['person_id'] === (int)Auth::personId()) { flash('err', '不能停用自己。'); redirect('people/show', ['id' => $personId]); } if ($status === 'disabled' && $acc['role'] === 'admin' && Account::adminCount() <= 1) { flash('err', '至少要留一位管理员。'); redirect('people/show', ['id' => $personId]); } Account::setStatus((int)$acc['id'], $status); flash('ok', $status === 'active' ? '账号已启用。' : '账号已停用。'); } redirect('people/show', ['id' => $personId]); break; // ==================== 设置 ==================== case 'settings': Auth::requireAdmin(); $dbVersion = '未知'; try { $dbVersion = Database::get()->query('SELECT VERSION()')->fetchColumn(); } catch (\Throwable $ex) { // 拿不到版本不影响页面 } $rawKey = (string)fb_config('app_key', ''); View::render('settings', [ 'title' => '设置', 'family' => Family::find((int)Auth::familyId()), 'dbVersion' => $dbVersion, 'keyOk' => $rawKey !== '' && strpos($rawKey, '请填写') !== 0, ], 'settings'); break; case 'settings/update': Auth::requireAdmin(); csrf_check(); Family::update( (int)Auth::familyId(), trim((string)($_POST['name'] ?? '家书')), trim((string)($_POST['motto'] ?? '')) ); flash('ok', '已保存。'); redirect('settings'); break; default: http_response_code(404); View::render('error', [ 'title' => '页面不存在', 'message' => '找不到这一页。', ], 'home'); break; }