新增存储抽象层:图片可存本地或云对象存储,切换存储方式不影响已有图片。
存储层
- 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 条踩坑记录、可直接复制的排障命令手册
136 lines
4.7 KiB
PHP
136 lines
4.7 KiB
PHP
<?php
|
|
// ============================================
|
|
// 文件: lib/db.php
|
|
// 说明: 数据库连接与初始化(建表 / 字段迁移 / 历史数据迁移 / 默认管理员)
|
|
// ============================================
|
|
|
|
// 数据库连接
|
|
function getDB() {
|
|
try {
|
|
$db = new PDO('sqlite:' . DB_PATH);
|
|
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
|
return $db;
|
|
} catch (PDOException $e) {
|
|
die('数据库连接失败: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
// 初始化数据库
|
|
function initDB() {
|
|
$db = getDB();
|
|
|
|
// 管理员表
|
|
$db->exec("
|
|
CREATE TABLE IF NOT EXISTS admins (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT UNIQUE NOT NULL,
|
|
password TEXT NOT NULL,
|
|
avatar TEXT
|
|
)
|
|
");
|
|
|
|
// 电影院表
|
|
$db->exec("
|
|
CREATE TABLE IF NOT EXISTS cinemas (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
place TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
address TEXT,
|
|
color TEXT DEFAULT '#888888'
|
|
)
|
|
");
|
|
|
|
// 电影表
|
|
$db->exec("
|
|
CREATE TABLE IF NOT EXISTS movies (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
title TEXT NOT NULL,
|
|
info TEXT,
|
|
remark TEXT,
|
|
poster_path TEXT,
|
|
ticket_path TEXT,
|
|
photo_path TEXT,
|
|
cinema_id INTEGER,
|
|
watch_date DATE,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (cinema_id) REFERENCES cinemas(id) ON DELETE SET NULL
|
|
)
|
|
");
|
|
|
|
// 观影人表
|
|
$db->exec("
|
|
CREATE TABLE IF NOT EXISTS persons (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
color TEXT DEFAULT '#888888',
|
|
avatar TEXT
|
|
)
|
|
");
|
|
|
|
// 观影关联表
|
|
$db->exec("
|
|
CREATE TABLE IF NOT EXISTS viewers (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
movie_id INTEGER NOT NULL,
|
|
name TEXT,
|
|
person_id INTEGER,
|
|
FOREIGN KEY (movie_id) REFERENCES movies(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (person_id) REFERENCES persons(id) ON DELETE SET NULL
|
|
)
|
|
");
|
|
|
|
// 多照片表
|
|
$db->exec("
|
|
CREATE TABLE IF NOT EXISTS movie_photos (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
movie_id INTEGER NOT NULL,
|
|
photo_path TEXT NOT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (movie_id) REFERENCES movies(id) ON DELETE CASCADE
|
|
)
|
|
");
|
|
|
|
// 系统设置表(键值结构,用于存储方式等配置)
|
|
$db->exec("
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
name TEXT PRIMARY KEY,
|
|
value TEXT,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
");
|
|
|
|
// ===== 迁移字段 =====
|
|
try { $db->exec("ALTER TABLE cinemas ADD COLUMN color TEXT DEFAULT '#888888'"); } catch (PDOException $e) {}
|
|
try { $db->exec("ALTER TABLE admins ADD COLUMN avatar TEXT"); } catch (PDOException $e) {}
|
|
try { $db->exec("ALTER TABLE persons ADD COLUMN avatar TEXT"); } catch (PDOException $e) {}
|
|
try { $db->exec("ALTER TABLE movies ADD COLUMN remark TEXT"); } catch (PDOException $e) {}
|
|
try { $db->exec("ALTER TABLE viewers ADD COLUMN person_id INTEGER REFERENCES persons(id) ON DELETE SET NULL"); } catch (PDOException $e) {}
|
|
// 新增:管理员关联观影人
|
|
try { $db->exec("ALTER TABLE admins ADD COLUMN person_id INTEGER REFERENCES persons(id) ON DELETE SET NULL"); } catch (PDOException $e) {}
|
|
|
|
// ===== 迁移旧数据 =====
|
|
// 将 viewers.name 迁移到 persons
|
|
$stmt = $db->query("SELECT DISTINCT name FROM viewers WHERE name != '' AND name IS NOT NULL");
|
|
$names = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
|
foreach ($names as $name) {
|
|
$chk = $db->prepare("SELECT id FROM persons WHERE name = ?");
|
|
$chk->execute([$name]);
|
|
if (!$chk->fetch()) {
|
|
$ins = $db->prepare("INSERT INTO persons (name) VALUES (?)");
|
|
$ins->execute([$name]);
|
|
}
|
|
}
|
|
$update = $db->prepare("UPDATE viewers SET person_id = (SELECT id FROM persons WHERE persons.name = viewers.name) WHERE person_id IS NULL AND name IS NOT NULL");
|
|
$update->execute();
|
|
|
|
// 默认管理员
|
|
$stmt = $db->prepare("SELECT COUNT(*) FROM admins");
|
|
$stmt->execute();
|
|
if ($stmt->fetchColumn() == 0) {
|
|
$hashed = password_hash('123456', PASSWORD_DEFAULT);
|
|
$stmt = $db->prepare("INSERT INTO admins (username, password) VALUES (?, ?)");
|
|
$stmt->execute(['admin', $hashed]);
|
|
}
|
|
}
|