新增存储抽象层:图片可存本地或云对象存储,切换存储方式不影响已有图片。
存储层
- lib/storage.php 存储门面:本地优先读取、URL 生成、上传/删除、批量迁移、
服务端读取(storageRead)与访问方式判定(storageAccessMode)
- lib/storage/{local,oss,cos,qiniu}.php 四种后端驱动;http.php 共用 cURL 客户端;
meta.php 实例元数据鉴权(ECS RAM 角色 / CVM CAM 角色,免密钥)
- lib/settings.php 设置读写;云厂商密钥以 enc:v1:gcm 密文落库(db/.settings.key)
- 文件名改为内容指纹(md5 前 12 位_6 位随机[_角色].ext),内容不变则名不变,
因此可下发 immutable 长缓存
两种图片访问方式
- 直连存储:storageUrl() 产出对象存储地址(桶需允许匿名读)
- 服务端代理:media.php 鉴权后由服务器签名读取并同源回吐,桶可保持私有
· 未登录返回 403 纯文本(刻意不用 302,否则 <img> 会拿到一段 HTML)
· 对象键白名单防目录穿越;Content-Type 按扩展名白名单决定 + nosniff(防同源 XSS)
· 透传 If-None-Match / Range,上游 304 直接短路;private + immutable 长缓存
· 鉴权后立即 session_write_close(),避免排他会话锁把并行图片请求串行化
内网 endpoint 判定(修「开了代理后慢到不可用」)
- 内网地址只在与桶同地域的 ECS 上可路由;跨地域时 DNS 能解析但 TCP 连不通,
无脑「内网优先」会让每次读取先白等满一次连接超时(默认 10 秒)
- 改为先比本机 ECS 地域与桶地域(零成本、结论确定)→ 不同即跳过;
结论落缓存;内网只给 2 秒连接超时,传输层失败立刻标记且短期内不再重试
- 「测试连通性」升级为 PUT → GET 回读 → DELETE,并显式报出内网判定结论
界面与页面
- settings.php 系统设置页:驱动选择、各云配置、图片访问方式、连接测试、迁移进度
- storage_api.php 提供 stat / migrate / test 三个 JSON 接口
- detail / edit / annual_report / persons 原先直接拼本地路径(file_exists / '/'.$path),
迁云后会整块不渲染或 404,统一改为走存储门面
- js/main.js 增加破图占位兜底;config.php 统一下发 Referrer-Policy: same-origin
文档
- README 新增《图片存储与对象存储》整章:设计约定、两种访问方式取舍表、
直连存储逐项配置说明(桶 ACL / 防盗链白名单 / 跨域 CORS 与 crossorigin /
强制下载 / endpoint 地域 / 自定义域名)与上线检查清单、服务端代理实现要点、
迁移步骤、12 条踩坑记录、可直接复制的排障命令手册
88 lines
4.0 KiB
PHP
88 lines
4.0 KiB
PHP
<?php
|
||
// ============================================
|
||
// 文件: nav.php (导航栏组件,显示用户头像)
|
||
// ============================================
|
||
|
||
if (!isset($admin)) {
|
||
if (isset($_SESSION['admin_id'])) {
|
||
require_once 'config.php';
|
||
$admin = getCurrentAdmin();
|
||
} else {
|
||
$admin = ['username' => '未登录'];
|
||
}
|
||
}
|
||
|
||
$current_page = basename($_SERVER['PHP_SELF']);
|
||
?>
|
||
<nav class="navbar">
|
||
<div class="nav-container">
|
||
<div class="nav-brand">
|
||
<span class="brand-icon">🎬</span>
|
||
<span class="brand-name">电影日志</span>
|
||
</div>
|
||
<div class="nav-actions">
|
||
<span class="nav-user">
|
||
<img src="<?= getAvatarUrl($admin['avatar'] ?? null, $admin['username'] ?? '') ?>" style="width:28px;height:28px;border-radius:50%;object-fit:cover;vertical-align:middle;margin-right:4px;">
|
||
<?= h($admin['username'] ?? '管理员') ?>
|
||
</span>
|
||
<a href="add.php" class="btn btn-primary btn-sm">➕ 添加电影</a>
|
||
<div class="nav-dropdown">
|
||
<button class="dropbtn" onclick="toggleDropdown(event)">☰ 菜单 <span class="arrow">▼</span></button>
|
||
<div class="dropdown-content" id="dropdownMenu">
|
||
<a href="index.php" <?= $current_page == 'index.php' ? 'class="active"' : '' ?>>
|
||
<span class="icon">🏠</span> 首页
|
||
</a>
|
||
<a href="search.php" <?= $current_page == 'search.php' ? 'class="active"' : '' ?>>
|
||
<span class="icon">🔍</span> 高级搜索
|
||
</a>
|
||
<a href="stats.php" <?= $current_page == 'stats.php' ? 'class="active"' : '' ?>>
|
||
<span class="icon">📊</span> 统计
|
||
</a>
|
||
<a href="annual_report.php" <?= $current_page == 'annual_report.php' ? 'class="active"' : '' ?>>
|
||
<span class="icon">📅</span> 年度报告
|
||
</a>
|
||
<div class="divider"></div>
|
||
<a href="cinemas.php" <?= $current_page == 'cinemas.php' ? 'class="active"' : '' ?>>
|
||
<span class="icon">🏛️</span> 影院管理
|
||
</a>
|
||
<a href="persons.php" <?= $current_page == 'persons.php' ? 'class="active"' : '' ?>>
|
||
<span class="icon">👥</span> 观影人管理
|
||
</a>
|
||
<a href="import.php" <?= $current_page == 'import.php' ? 'class="active"' : '' ?>>
|
||
<span class="icon">📥</span> CSV导入
|
||
</a>
|
||
<div class="divider"></div>
|
||
<a href="change_password.php" <?= $current_page == 'change_password.php' ? 'class="active"' : '' ?>>
|
||
<span class="icon">🔑</span> 修改密码
|
||
</a>
|
||
<a href="users.php" <?= $current_page == 'users.php' ? 'class="active"' : '' ?>>
|
||
<span class="icon">👤</span> 用户管理
|
||
</a>
|
||
<div class="divider"></div>
|
||
<a href="settings.php" <?= $current_page == 'settings.php' ? 'class="active"' : '' ?>>
|
||
<span class="icon">⚙️</span> 系统设置
|
||
</a>
|
||
<div class="divider"></div>
|
||
<a href="logout.php" style="color: #e74c3c;">
|
||
<span class="icon">🚪</span> 退出登录
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<script>
|
||
function toggleDropdown(e) {
|
||
e.stopPropagation();
|
||
var menu = document.getElementById('dropdownMenu');
|
||
menu.classList.toggle('show');
|
||
}
|
||
document.addEventListener('click', function(e) {
|
||
if (!e.target.closest('.nav-dropdown')) {
|
||
document.querySelectorAll('.nav-dropdown .dropdown-content').forEach(function(el) {
|
||
el.classList.remove('show');
|
||
});
|
||
}
|
||
});
|
||
</script>
|