这一版把「家」的地基铺完: 功能 - 安装向导 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」当成通过,这一版在线上真实抓到过一次
85 lines
3.0 KiB
PHP
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>
|