Files
FamilyBook/templates/home.php
T
laoyang 8eb19267bc 阶段 0 完整版:名册 + 账号 + 安装向导 + 设置(宣纸主题)
这一版把「家」的地基铺完:

功能
- 安装向导 install.php:建四张表 → 建家庭 → 建第一人(兼管理员),装完自删;
  「是否已装」以数据库四张表为准,不以文件是否存在为准
- 家庭名册:成员增删改查、头像上传(居中裁方 512×512 转 JPEG、finfo 校验、
  随机文件名)、生日自动算年龄与倒数天数
- 敏感信息加密存储:手机号双写(SHA-256 哈希用于登录 + AES-256-CBC 密文用于显示)、
  身份证只存密文;页面上永远只显示脱敏后的 138****8000 / 前 6 后 4
- 账号体系:人与账号分离(孩子可入名册但无需密码)、开通/停用/角色、
  防爆破(5 次失败锁 60 秒)、修改密码、最后一个管理员不可移除
- 设置页:家庭名称与座右铭、加密密钥状态
- 宣纸主题 UI:砖红取自图标,扁平、留白充足;移动端顶栏收紧不换行

安全
- 所有 POST 走 CSRF;输出全量 e() 转义;uploads/ 禁执行脚本
- app/ 与 templates/ 加 .htaccess 整体拒绝直连(直接请求模板会 Fatal 并
  把服务器绝对路径打在页面上 —— 这是线上真实抓到过的问题)
- 已安装的 install.php 返回 404;display_errors 线上关闭

工程
- 无框架无 Composer,query-string 路由(index.php?r=模块/动作),不依赖 rewrite
- 静态资源带 mtime+size 版本号,改版后浏览器不会吃旧缓存
- 配套部署与冒烟(_tooling/,不入库):部署脚本容忍 tar 对非属主目录的
  元数据回填失败;冒烟断言「状态码 + 正文无 PHP 错误 + </html> 收尾 +
  无绝对路径泄露 + 断言总数下限」—— 只看状态码会把「200 但正文中间
  Fatal」当成通过,这一版在线上真实抓到过一次
2026-09-30 00:49:53 +08:00

85 lines
3.0 KiB
PHP

<?php
$memberCount = count($people);
$openCount = 0;
foreach ($people as $p) { if (!empty($p['account_id'])) { $openCount++; } }
?>
<section class="hero">
<img class="hero-logo" src="<?= e(asset('/assets/icons/icon-192.png')) ?>" alt="">
<div>
<h1><?= e($family['name']) ?></h1>
<p class="motto"><?= e($family['motto'] ?: '一本用岁月写成的家谱') ?></p>
</div>
<div class="hero-stats">
<div class="stat"><b><?= (int)$memberCount ?></b><span>位家庭成员</span></div>
<div class="stat"><b><?= (int)$openCount ?></b><span>已开通登录</span></div>
<div class="stat"><b><?= count($birthdays) ?></b><span>近期生日</span></div>
</div>
</section>
<?php if ($birthdays): ?>
<section class="section">
<h2 class="section-title">接下来的日子 <span class="hint">两个月内</span></h2>
<div class="bdays">
<?php foreach ($birthdays as $b): ?>
<div class="bday">
<img src="<?= e(\App\Upload::avatarUrl($b['avatar'])) ?>" alt="">
<div>
<b><?= e($b['name']) ?></b>
<div class="d">
<?php if ($b['days_left'] === 0): ?>
就是今天,生日快乐
<?php else: ?>
还有 <?= (int)$b['days_left'] ?> 天过生日
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</section>
<?php endif; ?>
<section class="section">
<div class="page-head" style="margin-bottom:12px">
<h1 style="font-size:15px">家庭成员</h1>
<div class="actions">
<a class="btn btn-ghost btn-sm" href="<?= e(url('people')) ?>">管理名册</a>
<?php if (\App\Auth::isAdmin()): ?>
<a class="btn btn-sm" href="<?= e(url('people/new')) ?>">添加成员</a>
<?php endif; ?>
</div>
</div>
<?php if (!$people): ?>
<div class="empty">
<b>名册还是空的</b>
先把家里人加进来,之后他们就能出现在每一条记录里。
</div>
<?php else: ?>
<div class="people-grid">
<?php foreach ($people as $p): ?>
<a class="person" href="<?= e(url('people/show', ['id' => (int)$p['id']])) ?>">
<div class="ph"><img src="<?= e(\App\Upload::avatarUrl($p['avatar'])) ?>" alt=""></div>
<div class="pb">
<div class="pname"><?= e($p['name']) ?></div>
<div class="pmeta">
<?php if ($p['account_role'] === 'admin' && $p['account_status'] === 'active'): ?>
<span class="badge badge-admin">管理员</span>
<?php elseif (!empty($p['account_id'])): ?>
<span class="badge badge-open">已开通</span>
<?php else: ?>
<span class="badge">未开通</span>
<?php endif; ?>
<?php
$age = age_from($p['birthday'] ?? '');
if ($age !== null) { echo ' · ' . (int)$age . ' 岁'; }
?>
</div>
</div>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>