新增存储抽象层:图片可存本地或云对象存储,切换存储方式不影响已有图片。
存储层
- 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 条踩坑记录、可直接复制的排障命令手册
72 lines
2.7 KiB
JavaScript
72 lines
2.7 KiB
JavaScript
// ============================================
|
|
// 文件: js/main.js
|
|
// 说明: 全站公共脚本。add.php 与 edit.php 的「一起看的人 / 电影院」
|
|
// 表单联动逻辑集中在此,避免两个页面各写一份。
|
|
// ============================================
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
console.log('🎬 电影日志系统已加载');
|
|
});
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return '';
|
|
const d = new Date(dateStr);
|
|
return d.getFullYear() + '年' +
|
|
String(d.getMonth() + 1).padStart(2, '0') + '月' +
|
|
String(d.getDate()).padStart(2, '0') + '日';
|
|
}
|
|
|
|
// ---------- 一起看的人 ----------
|
|
|
|
// 下拉框切到「新增」时显示姓名输入框
|
|
function toggleNewViewer(select) {
|
|
var parent = select.parentElement;
|
|
var input = parent.querySelector('input[name="viewers_new[]"]');
|
|
if (select.value === 'new') {
|
|
input.style.display = 'block';
|
|
input.required = true;
|
|
} else {
|
|
input.style.display = 'none';
|
|
input.required = false;
|
|
input.value = '';
|
|
}
|
|
}
|
|
|
|
// 删除一行观影人(至少保留一行)
|
|
function removeViewerRow(btn) {
|
|
var container = document.getElementById('viewerContainer');
|
|
if (container.children.length > 1) {
|
|
btn.parentElement.remove();
|
|
}
|
|
}
|
|
|
|
// ---------- 电影院 ----------
|
|
|
|
// 下拉框切到「新建电影院」时显示新建表单
|
|
function toggleNewCinema() {
|
|
var select = document.getElementById('cinemaSelect');
|
|
var group = document.getElementById('newCinemaGroup');
|
|
group.style.display = select.value === 'new' ? 'block' : 'none';
|
|
}
|
|
|
|
// ---------- 图片破图回退 ----------
|
|
|
|
// 服务端代理模式下,图片需要登录才能读取(media.php 未登录返回 403)。
|
|
// 会话过期后页面上的图片会加载失败——统一替换为内联占位图,避免满屏破图。
|
|
// 注意:error 事件不冒泡,必须用捕获阶段监听;占位图用 data URI,不依赖任何文件。
|
|
(function () {
|
|
var PLACEHOLDER = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="160" height="200">' +
|
|
'<rect width="100%" height="100%" fill="#eceef2"/>' +
|
|
'<text x="50%" y="50%" font-size="13" fill="#9aa0a6" text-anchor="middle" ' +
|
|
'dominant-baseline="middle">图片不可用</text></svg>');
|
|
|
|
document.addEventListener('error', function (e) {
|
|
var el = e.target;
|
|
if (!el || el.tagName !== 'IMG') return;
|
|
if (el.dataset.fallbackApplied === '1') return; // 防死循环
|
|
el.dataset.fallbackApplied = '1';
|
|
el.src = PLACEHOLDER;
|
|
}, true);
|
|
})();
|