2026-09-21 08:20:56 +08:00
|
|
|
|
# MovieLog_Server · 电影全纪录
|
2026-09-21 00:06:43 +00:00
|
|
|
|
|
2026-09-21 08:20:56 +08:00
|
|
|
|
一个轻量级的个人/家庭观影记录系统。纯 PHP 编写,使用 SQLite 作为数据库,**无需任何框架、无需 Composer、无需配置数据库服务**,上传到支持 PHP 的空间即可运行。
|
|
|
|
|
|
|
|
|
|
|
|
> 记录每一帧感动 —— 观影记录、影院、同行人、票根与照片,以及属于你的年度观影报告。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 功能特性
|
|
|
|
|
|
|
|
|
|
|
|
### 观影记录管理
|
|
|
|
|
|
- **瀑布流首页**:按观影日期倒序展示所有电影,卡片显示海报、日期、影院(影院专属配色)、同行人头像。
|
|
|
|
|
|
- **添加电影**:支持电影名、剧情简介、观影日期、电影院、同行人、海报、票根、多张观影照片。
|
|
|
|
|
|
- **编辑 / 删除**:编辑页可替换或裁剪已有图片;删除电影时同步清理关联的图片文件。
|
|
|
|
|
|
- **电影详情**:海报大图、基本信息、备注、同行人列表(含头像)、影院信息、影像记录画廊(票根 + 观影照片,点击可查看原图)。
|
|
|
|
|
|
|
|
|
|
|
|
### 图片处理
|
|
|
|
|
|
- **内置裁剪器**:基于 Cropper.js,票根 / 海报 / 观影照片 / 观影人头像均可在浏览器内裁剪后再保存,服务端接收 Base64 图片落盘。
|
|
|
|
|
|
- **多图上传**:观影照片支持一次选择多张。
|
2026-09-21 22:21:01 +08:00
|
|
|
|
- **头像兜底**:未上传头像的观影人一律显示 `assets/default-avatar.png` 默认头像(人形剪影);若该资源缺失(例如只部署了 PHP 而漏了 `assets/`),`getAvatarUrl()` 会退化为内联 SVG 剪影 —— 保证任何情况下都不会出现"坏图"。
|
2026-09-21 08:20:56 +08:00
|
|
|
|
- 上传目录按类型分离:`uploads/posters/`、`uploads/tickets/`、`uploads/photos/`、`uploads/avatars/`。
|
2026-09-21 22:06:02 +08:00
|
|
|
|
- **破图兜底**:图片加载失败时由前端脚本替换为内联占位图,避免会话过期或权限异常时满屏破图刷屏控制台。
|
|
|
|
|
|
|
|
|
|
|
|
### 图片存储(本地 / 云对象存储)
|
|
|
|
|
|
- **四种存储后端**:本地硬盘(默认)、阿里云 OSS、腾讯云 COS、七牛云 Kodo,在「系统设置」页切换,**无需改代码**。
|
|
|
|
|
|
- **免密钥鉴权**:部署在阿里云 ECS / 腾讯云 CVM 上时,可直接绑定 RAM/CAM 角色读取实例元数据获取临时凭据,**不必在站内保存长期密钥**。
|
|
|
|
|
|
- **本地优先读取**:数据库里只存相对对象键(如 `uploads/posters/xxx.webp`);本地存在就本地读,否则读云端。**因此切换存储方式不会影响已有图片**,迁移可以慢慢来。
|
|
|
|
|
|
- **内容指纹命名**:文件名为 `md5(内容)前 12 位_6 位随机[_角色].ext`,内容不变则名字不变,可以放心下长缓存。
|
|
|
|
|
|
- **一键迁移**:把已有本地图片上传到云端并自动改写数据库引用(分批执行、带进度、幂等可重入)。
|
|
|
|
|
|
- **两种图片访问方式**:**直连存储**(浏览器直接访问对象存储)与**服务端代理**(经本站 `media.php` 鉴权后由服务器读取回吐,**桶可保持私有**)。两者的取舍与配置见下文《图片存储与对象存储》。
|
2026-09-21 08:20:56 +08:00
|
|
|
|
|
|
|
|
|
|
### 信息辅助
|
|
|
|
|
|
- **OMDb 自动获取**:在添加页输入片名后点击「📡 自动获取」,自动填充年份、类型、片长、导演、主演、IMDb 评分与剧情简介,并预览海报(海报仅供参考,仍需本地上传)。
|
|
|
|
|
|
- **CSV 批量导入**:一次性导入历史数据,自动识别 UTF-8 / GBK / GB2312 编码,自动创建不存在的影院,影院名去重,全程事务保护(出错自动回滚),并输出逐行错误报告。
|
|
|
|
|
|
|
|
|
|
|
|
### 检索与统计
|
|
|
|
|
|
- **首页快捷搜索**:按片名模糊搜索。
|
|
|
|
|
|
- **高级搜索**:片名 + 影院 + 观影人 + 日期区间,多条件组合筛选。
|
|
|
|
|
|
- **统计仪表板**:总览卡片(电影数 / 影院数 / 观影人数 / 观影人次 / 时间跨度),按影院、按观影人、按年份、最近 12 个月的条形图排行。
|
|
|
|
|
|
|
|
|
|
|
|
### 年度报告
|
|
|
|
|
|
- **个性化年度报告**:以「当前登录账号所关联的观影人」为视角生成,而非全站数据。
|
|
|
|
|
|
- **报告内容**:年度观影总数、观影天数、最常去的影院、最常一起观影的人、单场最多人的电影、首末场日期、月均频率、平均间隔天数、备注关键词云、今年共同观影人排行、年末最近观看、年度观影记录墙(含海报与同行人头像)。
|
|
|
|
|
|
- **可视化**:月度观影分布、星期观影偏好(Chart.js 双图),整体为深色"影院感"主题。
|
|
|
|
|
|
|
|
|
|
|
|
### 多用户与权限
|
|
|
|
|
|
- **Session 登录**:`password_hash` / `password_verify` 加盐哈希存储口令,全站页面均需登录。
|
|
|
|
|
|
- **用户管理**:可新增、编辑、删除管理员账号,可修改用户名与密码。
|
|
|
|
|
|
- **账号 ↔ 观影人绑定**:每个管理员账号可关联一个观影人,绑定后即可查看个人年度报告。
|
|
|
|
|
|
- **安全约束**:不允许删除最后一个管理员,也不允许删除自己。
|
|
|
|
|
|
- **个人资料**:修改自己的用户名与密码(需验证旧密码)。
|
|
|
|
|
|
|
|
|
|
|
|
### 基础数据维护
|
|
|
|
|
|
- **影院管理**:增删改查,支持「所在地方 / 影院名称 / 详细地址」三段式信息,内置 50 种预设配色,每个影院可用专属颜色区分;删除已被引用的影院时会给出醒目提示并要求二次确认(关联电影的影院字段将被置空)。
|
|
|
|
|
|
- **观影人管理**:增删改查,支持头像裁剪上传与专属配色;已被电影引用的观影人不允许直接删除。
|
|
|
|
|
|
|
|
|
|
|
|
### 界面与体验
|
|
|
|
|
|
- 复古票根色系(主色 `#e67e22` 橙)的响应式设计,移动端断点适配(600 / 768px)。
|
|
|
|
|
|
- 导航为下拉菜单,自动高亮当前页;顶部始终显示当前用户头像与用户名。
|
|
|
|
|
|
- 登录页为深色渐变 + 毛玻璃风格。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 技术栈
|
|
|
|
|
|
|
|
|
|
|
|
| 项目 | 说明 |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| 语言 | PHP(过程式 + PDO) |
|
|
|
|
|
|
| 数据库 | SQLite(`db/movies.db`,首次运行自动建表) |
|
|
|
|
|
|
| 前端 | 原生 HTML / CSS / JavaScript,无构建步骤 |
|
|
|
|
|
|
| 第三方库 | [Cropper.js 1.5.13](https://github.com/fengyuanchen/cropperjs)(图片裁剪)、[Chart.js 4.4.0](https://www.chartjs.org/)(图表),均由 CDN 引入 |
|
|
|
|
|
|
| 外部 API | [OMDb API](http://www.omdbapi.com/)(电影信息查询,需申请 API Key) |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 目录结构
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
MovieLog_Server/
|
2026-09-21 12:56:30 +08:00
|
|
|
|
├── config.php # 引导入口:错误报告、会话、路径常量、上传目录,并加载 lib/ 各模块
|
|
|
|
|
|
├── lib/ # 功能模块(被 config.php 统一引入,页面无需单独 require)
|
|
|
|
|
|
│ ├── db.php # DB 连接、建表、字段与历史数据迁移、默认管理员
|
|
|
|
|
|
│ ├── helpers.php # 输出转义、文件名、颜色校验、上传/删除文件、图片 URL
|
2026-09-21 22:06:02 +08:00
|
|
|
|
│ ├── settings.php # 系统设置读写;云厂商密钥以 enc:v1:gcm 密文落库
|
|
|
|
|
|
│ ├── storage.php # 存储门面:本地优先读取、URL 生成、上传/删除、迁移、代理读取
|
|
|
|
|
|
│ ├── storage/ # 各存储后端驱动
|
|
|
|
|
|
│ │ ├── local.php # 本地硬盘
|
|
|
|
|
|
│ │ ├── oss.php # 阿里云 OSS(签名 V1,支持 RAM 角色免密钥 / 内网 endpoint)
|
|
|
|
|
|
│ │ ├── cos.php # 腾讯云 COS
|
|
|
|
|
|
│ │ ├── qiniu.php # 七牛云 Kodo
|
|
|
|
|
|
│ │ ├── http.php # 共用 HTTP 客户端(cURL)与签名/编码工具
|
|
|
|
|
|
│ │ └── meta.php # 实例元数据鉴权(ECS RAM 角色 / CVM CAM 角色)与本机地域查询
|
2026-09-21 12:56:30 +08:00
|
|
|
|
│ ├── auth.php # 登录校验、管理员账号增删改
|
|
|
|
|
|
│ ├── movies.php # 电影/影院读取、搜索,以及表单关联数据的解析
|
|
|
|
|
|
│ └── stats.php # 统计仪表板与年度报告查询
|
2026-09-21 08:20:56 +08:00
|
|
|
|
├── index.php # 首页:观影列表(瀑布流)+ 快捷搜索
|
|
|
|
|
|
├── add.php # 添加电影
|
|
|
|
|
|
├── edit.php # 编辑电影
|
|
|
|
|
|
├── detail.php # 电影详情
|
|
|
|
|
|
├── delete.php # 删除电影(连带清理图片文件)
|
|
|
|
|
|
├── search.php # 高级搜索
|
|
|
|
|
|
├── stats.php # 统计仪表板
|
|
|
|
|
|
├── annual_report.php # 年度报告
|
|
|
|
|
|
├── cinemas.php # 影院管理
|
|
|
|
|
|
├── persons.php # 观影人管理
|
|
|
|
|
|
├── import.php # CSV 批量导入
|
|
|
|
|
|
├── users.php # 用户(管理员)管理
|
|
|
|
|
|
├── change_password.php # 修改自己的用户名/密码
|
|
|
|
|
|
├── login.php # 登录
|
|
|
|
|
|
├── logout.php # 退出
|
|
|
|
|
|
├── nav.php # 导航栏组件(被各页面 include)
|
2026-09-21 22:06:02 +08:00
|
|
|
|
├── settings.php # 系统设置页:存储后端选择、图片访问方式、连接测试、迁移
|
|
|
|
|
|
├── storage_api.php # 存储维护接口(stat / migrate / test),返回 JSON
|
|
|
|
|
|
├── media.php # 服务端图片代理:鉴权后由服务器读取对象并同源回吐
|
2026-09-21 08:20:56 +08:00
|
|
|
|
├── fetch_movie.php # OMDb 代理接口,返回 JSON
|
|
|
|
|
|
├── test_crop.php # 开发调试页:验证 Base64 图片落盘是否正常
|
|
|
|
|
|
├── css/style.css # 全局样式
|
2026-09-21 22:06:02 +08:00
|
|
|
|
├── js/main.js # 公共脚本(含各页面共用的表单联动函数、破图兜底)
|
2026-09-21 22:21:01 +08:00
|
|
|
|
├── assets/ # 静态资源
|
|
|
|
|
|
│ └── default-avatar.png # 默认头像(未上传头像时统一使用)
|
2026-09-21 08:20:56 +08:00
|
|
|
|
├── db/movies.db # SQLite 数据库(运行时自动生成,不纳入版本控制)
|
|
|
|
|
|
└── uploads/ # 上传图片(运行时自动生成,不纳入版本控制)
|
|
|
|
|
|
├── posters/ # 海报
|
|
|
|
|
|
├── tickets/ # 票根
|
|
|
|
|
|
├── photos/ # 观影照片
|
|
|
|
|
|
└── avatars/ # 观影人头像
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-09-21 12:56:30 +08:00
|
|
|
|
> 页面只需 `require_once 'config.php'`,`lib/` 下的模块会由引导文件统一加载,并保证依赖顺序(db → helpers → auth → movies → stats)。
|
|
|
|
|
|
|
2026-09-21 08:20:56 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 数据库结构
|
|
|
|
|
|
|
2026-09-21 22:06:02 +08:00
|
|
|
|
建表逻辑集中在 `lib/db.php` 的 `initDB()`(由 `config.php` 在每次请求引导时调用),每次请求都会执行 `CREATE TABLE IF NOT EXISTS` 与幂等的 `ALTER TABLE` 迁移,因此**升级代码无需手动改库**。
|
2026-09-21 08:20:56 +08:00
|
|
|
|
|
|
|
|
|
|
| 表 | 作用 | 关键字段 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| `admins` | 管理员账号 | `username`(唯一)、`password`(哈希)、`avatar`、`person_id`(关联观影人) |
|
|
|
|
|
|
| `cinemas` | 电影院 | `place`(所在地方)、`name`、`address`、`color`(专属配色) |
|
|
|
|
|
|
| `movies` | 电影记录 | `title`、`info`、`remark`、`poster_path`、`ticket_path`、`photo_path`(历史遗留单图)、`cinema_id`、`watch_date`、`created_at` |
|
|
|
|
|
|
| `persons` | 观影人 | `name`(唯一)、`color`、`avatar` |
|
|
|
|
|
|
| `viewers` | 电影 ↔ 观影人关联 | `movie_id`、`person_id`、`name`(历史遗留字段) |
|
|
|
|
|
|
| `movie_photos` | 电影多图 | `movie_id`、`photo_path`、`created_at` |
|
2026-09-21 22:06:02 +08:00
|
|
|
|
| `settings` | 键值配置与缓存 | `name`(唯一)、`value`。存存储驱动选择、各云厂商配置、图片访问方式等;**云厂商密钥以 `enc:v1:gcm:` 前缀密文落库**(密钥文件 `db/.settings.key`,首次保存时自动生成,**丢失后密文不可恢复**) |
|
|
|
|
|
|
|
|
|
|
|
|
`settings` 中除配置外的运行时缓存键(都可安全清空,程序会自动重建):
|
|
|
|
|
|
|
|
|
|
|
|
| 键 | 含义 | 有效期 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| `cache_meta_aliyun` / `cache_meta_tencent` | 实例角色换来的临时凭据 | 到期前 5 分钟失效 |
|
|
|
|
|
|
| `cache_ecs_region` | 本机 ECS 所在地域(用于判断内网 endpoint 是否可用) | 1 小时 |
|
|
|
|
|
|
| `cache_oss_internal` | 内网 endpoint 可用性判定结论 | 可用 1 小时 / 不可用 30 分钟 |
|
2026-09-21 08:20:56 +08:00
|
|
|
|
|
|
|
|
|
|
外键约束:`movies.cinema_id → cinemas.id`(`ON DELETE SET NULL`)、`viewers.movie_id → movies.id`(`ON DELETE CASCADE`)、`movie_photos.movie_id → movies.id`(`ON DELETE CASCADE`)。
|
|
|
|
|
|
|
|
|
|
|
|
**历史数据迁移**:`initDB()` 会自动把早期版本存在 `viewers.name` 里的姓名去重后写入 `persons`,并回填 `viewers.person_id`,老库可直接升级。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 安装部署
|
|
|
|
|
|
|
|
|
|
|
|
### 环境要求
|
|
|
|
|
|
|
|
|
|
|
|
- PHP 7.4+(推荐 8.x),需启用扩展:`pdo_sqlite`、`fileinfo`、`mbstring`、`curl`
|
|
|
|
|
|
- Web 服务器:Apache / Nginx / PHP 内置服务器均可
|
2026-09-21 22:06:02 +08:00
|
|
|
|
- **使用对象存储时额外需要**:
|
|
|
|
|
|
- `curl` 扩展可用,且系统存在可信 CA 证书(`/etc/ssl/certs/ca-certificates.crt`、`/etc/pki/tls/certs/ca-bundle.crt` 等常见路径之一)。找不到 CA 时程序会回退给 curl 自行处理,但部分精简系统需要手动安装 `ca-certificates`,否则 HTTPS 访问对象存储会失败。
|
|
|
|
|
|
- 若使用「实例角色(免密钥)」鉴权:服务器需运行在对应云上,且实例已绑定 RAM(阿里云)/ CAM(腾讯云)角色。
|
|
|
|
|
|
- **`settings.php` 会写入 `db/`**:请确保 Web 用户对 `db/` 有写权限(密钥文件 `db/.settings.key` 首次保存时在此生成)。
|
2026-09-21 08:20:56 +08:00
|
|
|
|
|
|
|
|
|
|
### 步骤
|
|
|
|
|
|
|
|
|
|
|
|
1. **放置代码**:将项目文件放到 Web 根目录或其子目录。
|
|
|
|
|
|
|
|
|
|
|
|
2. **确保可写**:保证项目根目录可写,程序会自动创建 `db/` 与 `uploads/` 及其子目录;或手动创建并授权:
|
|
|
|
|
|
```bash
|
|
|
|
|
|
mkdir -p db uploads/posters uploads/tickets uploads/photos uploads/avatars
|
|
|
|
|
|
chmod -R 755 db uploads
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
3. **配置 OMDb API Key**(可选,仅影响「自动获取」功能):编辑 `fetch_movie.php`,将 `OMDB_API_KEY` 换成你在 [omdbapi.com](http://www.omdbapi.com/apikey.aspx) 申请的免费 Key。
|
|
|
|
|
|
|
|
|
|
|
|
4. **访问站点**:浏览器打开 `login.php`。
|
|
|
|
|
|
|
|
|
|
|
|
5. **首次登录**:数据库为空时会自动创建默认管理员:
|
|
|
|
|
|
- 用户名 `admin`
|
|
|
|
|
|
- 密码 `123456`
|
|
|
|
|
|
> **登录后请立即在「修改密码」中更换。**
|
|
|
|
|
|
|
|
|
|
|
|
### 本地快速运行
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
php -S localhost:8000
|
|
|
|
|
|
# 浏览器访问 http://localhost:8000/login.php
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-09-21 22:06:02 +08:00
|
|
|
|
> 用 PHP 内置服务器时,`posterUrl()`(`lib/helpers.php`)与 `storageUrl()`(`lib/storage.php`)返回的是以 `/` 开头的站点绝对路径(如 `/uploads/posters/xxx.webp`),建议从项目根目录启动,或通过站点根目录访问。
|
2026-09-21 08:20:56 +08:00
|
|
|
|
|
|
|
|
|
|
### Nginx 参考配置
|
|
|
|
|
|
|
|
|
|
|
|
```nginx
|
|
|
|
|
|
server {
|
|
|
|
|
|
listen 80;
|
|
|
|
|
|
server_name movielog.local;
|
|
|
|
|
|
root /path/to/MovieLog_Server;
|
|
|
|
|
|
index index.php;
|
|
|
|
|
|
|
|
|
|
|
|
location / {
|
|
|
|
|
|
try_files $uri $uri/ =404;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
location ~ \.php$ {
|
|
|
|
|
|
fastcgi_pass 127.0.0.1:9000;
|
|
|
|
|
|
fastcgi_index index.php;
|
|
|
|
|
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
|
|
|
|
|
include fastcgi_params;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 使用说明
|
|
|
|
|
|
|
|
|
|
|
|
### 添加一部电影
|
|
|
|
|
|
|
|
|
|
|
|
1. 顶部导航点击「➕ 添加电影」。
|
|
|
|
|
|
2. 输入片名后点「📡 自动获取」可自动填充影片信息(可选)。
|
|
|
|
|
|
3. 填写观影日期(必填)、选择或新建电影院。
|
|
|
|
|
|
4. 「一起看的人」逐行添加:选择已有观影人,或在下拉框中选「➕ 新增」并输入姓名,新姓名会自动进入观影人库。
|
|
|
|
|
|
5. 上传海报(推荐 2:3);上传票根会弹出裁剪框,裁剪结果以 Base64 提交。
|
|
|
|
|
|
6. 观影照片支持一次多选。
|
|
|
|
|
|
7. 保存后自动跳转到详情页。
|
|
|
|
|
|
|
|
|
|
|
|
### 生成年度报告
|
|
|
|
|
|
|
|
|
|
|
|
年度报告是**按人**生成的,需要先建立账号与观影人的绑定:
|
|
|
|
|
|
|
|
|
|
|
|
1. 进入「用户管理」,找到自己的账号,在「关联观影人」中选择对应的观影人并保存。
|
|
|
|
|
|
2. 进入「年度报告」,页面顶部切换年份即可查看。
|
|
|
|
|
|
3. 未绑定观影人时报告为空并给出提示。
|
|
|
|
|
|
|
|
|
|
|
|
### CSV 批量导入
|
|
|
|
|
|
|
|
|
|
|
|
CSV **共 9 列,顺序固定**:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
电影名称, 地点(忽略), 电影院名称, 观影日期, 观影人(空格分隔), 电影票图片路径, 海报图片路径, 备注, 合影图片路径
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- **观影人**:多个姓名用空格分隔,例如 `张三 李四 王五`。
|
|
|
|
|
|
- **图片路径**:可带目录,系统只取最后一个 `/` 后的文件名;若整段被括号包裹(如 `(电影票/阿甘正传.jpg)`),会先取括号内内容再取文件名。
|
|
|
|
|
|
- **观影日期可为空**,空日期的记录会排在列表最后。
|
|
|
|
|
|
- **导入前**需先把图片文件放入 `uploads/tickets/`、`uploads/posters/`、`uploads/photos/`,文件名与 CSV 中提取出的保持一致。
|
|
|
|
|
|
- 编码支持 UTF-8 / GBK / GB2312,自动识别转换。
|
|
|
|
|
|
- 导入在事务中执行,失败会整体回滚;成功后显示「导入 N 条 / 跳过 M 条」及逐行错误原因。
|
|
|
|
|
|
|
|
|
|
|
|
示例行:
|
|
|
|
|
|
|
|
|
|
|
|
```csv
|
|
|
|
|
|
阿甘正传, , 万达影城, 2024-01-15, 张三 李四, (电影票/阿甘正传.jpg), (海报/阿甘正传.jpg), 经典励志片, (合影/阿甘正传.jpg)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-09-21 22:06:02 +08:00
|
|
|
|
### 把图片放到对象存储(可选)
|
|
|
|
|
|
|
|
|
|
|
|
不改也能用(默认本地硬盘)。想用云存储时:
|
|
|
|
|
|
|
|
|
|
|
|
1. 进入**系统设置**,选择存储后端(阿里云 OSS / 腾讯云 COS / 七牛云 Kodo),填写 Bucket、Endpoint、对象前缀等。
|
|
|
|
|
|
2. 选择鉴权方式:
|
|
|
|
|
|
- **实例角色(免密钥,推荐)**:服务器在对应云上并绑定了角色时选它,不用在站内保存任何长期密钥。
|
|
|
|
|
|
- **AccessKey**:手动填写密钥,入库即加密。
|
|
|
|
|
|
3. 点**测试连通性**——它会执行「写入探针 → **读回校验** → 删除探针」,因此**同时验证了写和读两条链路**,并会明确报出内网 endpoint 的判定结论。
|
|
|
|
|
|
4. 选择**图片访问方式**(直连存储 / 服务端代理),两个选项的取舍见《图片存储与对象存储》§2。
|
|
|
|
|
|
5. 需要的话点**迁移**,把已有本地图片搬到云端(分批、可重入,建议先备份 `db/` 与 `uploads/`)。
|
|
|
|
|
|
6. 迁移完成后**全站看图确认无误,再清理本地文件**——读取是本地优先的。
|
|
|
|
|
|
|
2026-09-21 08:20:56 +08:00
|
|
|
|
### 接口:`fetch_movie.php`
|
|
|
|
|
|
|
|
|
|
|
|
供添加页前端调用,服务端代请求 OMDb 避免浏览器跨域。
|
|
|
|
|
|
|
|
|
|
|
|
- **请求**:`GET fetch_movie.php?q=电影名称`
|
|
|
|
|
|
- **响应**:`Content-Type: application/json`
|
|
|
|
|
|
- **成功**:
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"title": "Forrest Gump",
|
|
|
|
|
|
"year": "1994",
|
|
|
|
|
|
"rated": "PG-13",
|
|
|
|
|
|
"released": "06 Jul 1994",
|
|
|
|
|
|
"runtime": "142 min",
|
|
|
|
|
|
"genre": "Drama, Romance",
|
|
|
|
|
|
"director": "Robert Zemeckis",
|
|
|
|
|
|
"actors": "Tom Hanks, Robin Wright, Gary Sinise",
|
|
|
|
|
|
"plot": "...",
|
|
|
|
|
|
"poster": "https://...",
|
|
|
|
|
|
"imdbRating": "8.8"
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
- **失败**:`{"error": "错误说明"}`(如「未找到相关电影,请尝试更准确的关键词」)
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-09-21 22:06:02 +08:00
|
|
|
|
## 图片存储与对象存储(OSS / COS / 七牛)
|
|
|
|
|
|
|
|
|
|
|
|
> **这一节回答两个问题**:① 想用「直连存储」,对象存储那边到底要怎么配?② 我们在这条路上踩过的坑分别长什么样、怎么排。
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 几个设计约定(先理解这个,后面的现象才好解释)
|
|
|
|
|
|
|
|
|
|
|
|
| 约定 | 说明 |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| **库里只存相对对象键** | `movies.poster_path` 等字段保存的是 `uploads/posters/xxxx.webp` 这种**相对键**,不含域名、不含 bucket、不含前缀。所以改域名 / 换 bucket / 改前缀都不用动数据库 |
|
|
|
|
|
|
| **本地优先读取** | `storageUrl()` / `storageExists()` 先看本地 `uploads/` 里有没有这个文件:有 → 走站内路径 `/uploads/...`;没有 → 交给当前存储驱动。**这是「切换存储方式不影响已有图片」和「可以慢慢迁移」的根本原因** |
|
|
|
|
|
|
| **内容指纹命名** | 文件名为 `md5(内容)前 12 位_6 位随机[_角色].ext`。内容不变则名字不变 → 可以下发 `immutable` 长缓存 |
|
|
|
|
|
|
| **门面 + 驱动** | 页面只调用 `lib/storage.php` 门面(`storageUrl` / `storageExists` / `storagePutFile` / `storageDelete` / `storageRead` / `storageAccessMode` …),不直接碰 SDK。加云厂商 = 加一个 `lib/storage/*.php` 驱动 |
|
|
|
|
|
|
| **密钥加密落库** | 云厂商 AccessKey 以 `enc:v1:gcm:` 密文存进 `settings` 表,密钥文件 `db/.settings.key`。**该文件丢失后密文不可恢复**,只能重新填写密钥。用实例角色(免密钥)则不存在这个问题 |
|
|
|
|
|
|
| **对象前缀** | 桶内可再套一层目录(如 `movielog/`)。前缀只在驱动层拼接,库中键始终干净。同一个桶给多个站点用时靠它隔离 |
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 两种图片访问方式:怎么选
|
|
|
|
|
|
|
|
|
|
|
|
| | **直连存储**(direct) | **服务端代理**(proxy) |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| 图片 URL | `https://<bucket>.oss-….aliyuncs.com/movielog/uploads/…` | `/media.php?k=uploads/…`(同源) |
|
|
|
|
|
|
| 桶权限要求 | **必须公共读**(或对每个对象签名,但那样 URL 会过期) | **可以私有**,服务器用实例角色/AK 签名读取 |
|
|
|
|
|
|
| 防盗链白名单 | 需要包含本站域名,或保持「允许空 Referer」(本项目统一下发 `Referrer-Policy: same-origin`,跨域请求不带 Referer) | 不需要(浏览器根本不访问对象存储) |
|
|
|
|
|
|
| 跨域 CORS | 只有想在浏览器里**读像素**(裁剪)时才需要 | **完全不需要**(同源) |
|
|
|
|
|
|
| 谁能看到图 | 任何拿到 URL 的人 | 必须登录本站 |
|
|
|
|
|
|
| 流量路径 | 浏览器 ⇄ 对象存储(不走服务器带宽) | 浏览器 ⇄ 你的服务器 ⇄ 对象存储(**双倍流量、占服务器带宽**) |
|
|
|
|
|
|
| 额外延迟 | 无 | 每张图多一次服务器到存储的往返(同地域约 10–30 ms;跨地域约 60–100 ms) |
|
|
|
|
|
|
| 适用 | 对性能敏感、图片可公开、愿意维护桶策略 | **希望桶保持私有**、不想维护防盗链/CORS、图片需要登录才可见 |
|
|
|
|
|
|
|
|
|
|
|
|
> **建议**:个人/家庭站点优先选**服务端代理**——桶私有、免防盗链、免 CORS、裁剪天然可用。只有当图片确实可以公开、且希望省下服务器中转带宽与流量时,才用直连。
|
|
|
|
|
|
>
|
|
|
|
|
|
> 选择位置:**系统设置 → 图片访问方式**。切换后无需重启,立即生效(回滚就是切回另一项)。
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 直连存储:对象存储必须怎么配(重点)
|
|
|
|
|
|
|
|
|
|
|
|
直连模式下,图片是**浏览器直接去对象存储取**的。因此下面每一项都会直接影响「图片能不能显示」。逐项过一遍。
|
|
|
|
|
|
|
|
|
|
|
|
#### ① 桶读写权限:必须允许匿名读取
|
|
|
|
|
|
|
|
|
|
|
|
浏览器请求对象存储时**不带任何身份**,所以桶(或该目录下的对象)必须允许匿名 GET。
|
|
|
|
|
|
|
|
|
|
|
|
| 云 | 控制台位置 | 要选的值 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| 阿里云 OSS | Bucket → **读写权限**(ACL) | **公共读** |
|
|
|
|
|
|
| 腾讯云 COS | 存储桶 → **权限管理** → 访问权限 | **公有读私有写** |
|
|
|
|
|
|
| 七牛 Kodo | 空间设置 → 访问控制 | **公开** |
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ 改成公共读意味着**任何人拿到 URL 就能看图**。如果存的是私人内容,请用服务端代理而不是直连。
|
|
|
|
|
|
|
|
|
|
|
|
#### ② 防盗链(Referer 白名单):开了就必须放行本站
|
|
|
|
|
|
|
|
|
|
|
|
防盗链是对象存储的「只允许特定来源引用」策略。**开了但没把本站域名加进白名单,就会出现最迷惑的现象**:
|
|
|
|
|
|
|
|
|
|
|
|
> 把图片 URL 直接粘到浏览器地址栏 → **能打开**;同一个 URL 放在页面 `<img>` 里 → **403**。
|
|
|
|
|
|
|
|
|
|
|
|
原因是浏览器对子资源(`<img>`/`<script>`)会带上当前页面的 `Referer`,而地址栏直接访问不带。**这几乎总是防盗链造成的,不是图片坏了。**
|
|
|
|
|
|
|
|
|
|
|
|
| 云 | 控制台位置 |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| 阿里云 OSS | Bucket → **数据安全** → 防盗链 |
|
|
|
|
|
|
| 腾讯云 COS | 存储桶 → **安全管理** → 防盗链设置 |
|
|
|
|
|
|
| 七牛 Kodo | 域名管理 → **防盗链** |
|
|
|
|
|
|
|
|
|
|
|
|
配置要点:
|
|
|
|
|
|
|
|
|
|
|
|
1. 把站点域名加进白名单,例如 `movie.example.com`,需要子域时写 `*.example.com`。
|
|
|
|
|
|
2. **不要图省事直接关掉防盗链** —— 如果这个桶还给别的用途(比如当备份盘)用,加白名单是加法,更安全。
|
|
|
|
|
|
3. 确认「**允许空 Referer**」是开着的(各家默认开启)。本项目 `config.php` 里统一下发了 `Referrer-Policy: same-origin`,即**跨域请求不带 Referer、同源请求照常带**,所以即使白名单里漏配了本站,页面里的图片也能正常显示。这算一层兜底,但**不要拿它替代白名单**。
|
|
|
|
|
|
|
|
|
|
|
|
#### ③ 跨域 CORS:只有「裁剪 / 读像素」才需要
|
|
|
|
|
|
|
|
|
|
|
|
这不是防盗链,是**另一个开关**,两者经常被混为一谈。
|
|
|
|
|
|
|
|
|
|
|
|
浏览器里 `cropper.getCroppedCanvas()` + `canvas.toDataURL()` 需要**读取图片的像素**。如果图片来自别的域名且未获授权,Canvas 会被标记为「被污染」(tainted),读取时抛 `SecurityError`,表现就是「点了裁剪保存没反应 / 弹错误」。
|
|
|
|
|
|
|
|
|
|
|
|
**要让直连模式下的裁剪可用,必须同时满足两件事**(缺一不可):
|
|
|
|
|
|
|
|
|
|
|
|
1. **桶上配置 CORS 规则**:来源 `https://你的域名`,方法 `GET`(`HEAD` 建议一起),允许的请求头建议 `*`,暴露的响应头至少 `ETag`、`Content-Length`,缓存时间 600 秒。
|
|
|
|
|
|
2. **图片标签声明跨域模式**:`<img src="…" crossorigin="anonymous">`。**只配 CORS 而不加这个属性,Canvas 依然会被污染**;反之只加属性而不配 CORS,图片会**直接加载失败**(比不能裁剪更糟)。
|
|
|
|
|
|
|
|
|
|
|
|
> ⚠️ **本仓库当前没有给图片加 `crossorigin="anonymous"`**,因此**直连存储模式下裁剪功能不可用**,只能上传原图、不能二次裁剪。
|
|
|
|
|
|
> 如果你需要裁剪,最省事的做法是**改用服务端代理**(图片同源,两个条件全免);坚持直连则要自行在 `add.php` / `edit.php` / `persons.php` 的裁剪预览 `<img>` 上加 `crossorigin="anonymous"`,并先在桶上把 CORS 配对。
|
|
|
|
|
|
|
|
|
|
|
|
#### ④ 强制下载(`x-oss-force-download` / `Content-Disposition: attachment`)
|
|
|
|
|
|
|
|
|
|
|
|
桶级「强制下载」**不影响 `<img>` 正常显示**(浏览器对子资源忽略该头),但会让「点击图片看原图」变成弹下载框。想恢复成预览就把桶的强制下载关掉。
|
|
|
|
|
|
|
|
|
|
|
|
#### ⑤ Endpoint 必须与桶所在地域一致
|
|
|
|
|
|
|
|
|
|
|
|
`oss-cn-beijing.aliyuncs.com` 这类 endpoint 里的地域**必须和桶实际所在地域对应**,否则报
|
|
|
|
|
|
`The bucket you are attempting to access must be addressed using the specified endpoint`。
|
|
|
|
|
|
Endpoint 在 OSS 控制台「概览」页可以看到,填写时**不要带 `https://`**。
|
|
|
|
|
|
|
|
|
|
|
|
#### ⑥ 自定义域名 / CDN(可选)
|
|
|
|
|
|
|
|
|
|
|
|
绑定了自定义域名或 CDN 才填;填了之后图片 URL 会使用该域名。**填一个没绑定的域名会直接报错**(`CnameDenied` / `SecondLevelDomainForbidden`)。七牛云**必须**绑定域名(它不提供默认外链域名)。
|
|
|
|
|
|
|
|
|
|
|
|
#### ⑦ 直连模式检查清单
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] 桶 ACL = 公共读 / 公有读私有写 / 公开
|
|
|
|
|
|
- [ ] 防盗链白名单包含本站域名,或该桶未开防盗链
|
|
|
|
|
|
- [ ] 桶地域与填写的 endpoint 地域一致
|
|
|
|
|
|
- [ ] 系统设置页点「测试连通性」→ 提示可读写
|
|
|
|
|
|
- [ ] 首页 / 详情 / 编辑预览 / 统计 / 年度报告 / 头像 逐页看图
|
|
|
|
|
|
- [ ] 需要裁剪的话:桶 CORS + `<img crossorigin="anonymous">` 两者都到位
|
|
|
|
|
|
- [ ] 知道这个桶的图片对任何拿到 URL 的人可见
|
|
|
|
|
|
|
|
|
|
|
|
### 4. 服务端代理是怎么回事
|
|
|
|
|
|
|
|
|
|
|
|
图片 URL 变成 `/media.php?k=uploads/posters/xxx.webp`,请求打到本站,由服务器去对象存储取,再**同源**吐给浏览器。因此:桶可以私有、不需要防盗链、不需要 CORS。
|
|
|
|
|
|
|
|
|
|
|
|
`media.php` 做对的几件事(都值得保留,改动时别退化):
|
|
|
|
|
|
|
|
|
|
|
|
- **未登录直接 403 纯文本**(刻意不用 302 跳登录页 —— 否则 `<img>` 会拿到一段 HTML,控制台刷满「HTML 当图片解析失败」的噪声,掩盖真正原因)。
|
|
|
|
|
|
- **鉴权后立刻 `session_write_close()`**:PHP 的会话文件锁是**排他**的,如果一直持有到脚本结束,浏览器并行为一张张图片开的连接会全部串行等待,页面图片越多越慢(见下方坑 5)。
|
|
|
|
|
|
- **对象键白名单**:只允许 `uploads/{posters,tickets,photos,avatars}/` 下的纯文件名,并显式拒绝 `..`。否则 `?k=../config.php` 就是任意文件读取。
|
|
|
|
|
|
- **Content-Type 由本地扩展名白名单决定**,绝不采信上游返回的类型。否则桶里若被塞进 `.html` / `.svg`,就会以 `text/html` 在**本站域名下**执行(同源 XSS)。同时下发 `X-Content-Type-Options: nosniff`。
|
|
|
|
|
|
- **条件请求透传**:`If-None-Match` / `If-Modified-Since` / `Range` 转给上游,上游回 304 就短路返回,省流量。
|
|
|
|
|
|
- **长缓存**:对象名是内容指纹,永不原地变更,所以下发 `Cache-Control: private, max-age=31536000, immutable`(`private` 必须有,因为内容是按登录会话授权的)。
|
|
|
|
|
|
|
|
|
|
|
|
#### 内网 endpoint 只在与桶同地域时可用
|
|
|
|
|
|
|
|
|
|
|
|
服务器与桶**在同一地域**时,把请求地址换成 `oss-<地域>-internal.aliyuncs.com` 可以走内网:**流量免费、延迟更低**。设置页 OSS 面板有「服务端读取走内网 endpoint」开关。
|
|
|
|
|
|
|
|
|
|
|
|
但注意:**内网 endpoint 只在与桶同地域的 ECS 上可路由**。跨地域时 DNS 会照常解析出一个地址(毫秒级,看起来毫无异常),TCP 却**永远连不上**。所以程序会:
|
|
|
|
|
|
|
|
|
|
|
|
1. 先比**本机 ECS 地域**(实例元数据 `region-id`)与**桶地域**,不同就直接跳过内网(零成本、结论确定);
|
|
|
|
|
|
2. 地域相同或无法判定时,才真的发一个签名 `HEAD` 到桶根探测一次(连接超时 2 秒);
|
|
|
|
|
|
3. 结论缓存起来,并显示在**系统设置 → 当前状态 →「服务端读取通道」**;
|
|
|
|
|
|
4. 万一判断失效,内网尝试只等 2 秒就回退公网(不是默认的 10 秒)。
|
|
|
|
|
|
|
|
|
|
|
|
### 5. 迁移已有图片到云端
|
|
|
|
|
|
|
|
|
|
|
|
**系统设置 → 图片存储 → 迁移**:
|
|
|
|
|
|
|
|
|
|
|
|
1. 先把本地 `uploads/` 与 `db/` **备份一份**(迁移会改写数据库里的图片引用)。
|
|
|
|
|
|
2. 迁移是**分批**执行的(前端循环调用 `storage_api.php?action=migrate` 并显示进度),中断了可以直接重跑——已迁移的记录会被跳过,不会重复计数。
|
|
|
|
|
|
3. 迁移过程中会把文件重命名为内容指纹名,并**同步改写数据库引用**。
|
|
|
|
|
|
4. 迁移完成后**本地文件不会自动删除**,因为读取是「本地优先」的:本地还在就继续走本地,删掉本地文件后才真正走云端。**删本地文件前先全站看图确认。**
|
|
|
|
|
|
|
|
|
|
|
|
### 6. 踩坑记录(按现象索引)
|
|
|
|
|
|
|
|
|
|
|
|
#### 坑 1 · 图片在页面上 403,URL 粘到地址栏却能打开
|
|
|
|
|
|
**对象存储的防盗链(Referer 白名单)**。浏览器对 `<img>` 会带 `Referer`,地址栏直粘不带。
|
|
|
|
|
|
→ 解法见 §3②(加白名单);临时绕法是在统一引导文件下发 `Referrer-Policy: same-origin`(本项目已做);根治是改用服务端代理。
|
|
|
|
|
|
**诊断**:`curl` 分别带 / 不带 `Referer` 各打一次,对比状态码,并把 403 的响应体打出来 —— 里面会直接写 `You are denied by bucket referer policy.` 之类的原始判决。
|
|
|
|
|
|
|
|
|
|
|
|
#### 坑 2 · 迁到云端之后,某些图「整块消失」或 404
|
|
|
|
|
|
代码还在把数据库里的**对象键当本地路径**拼:`file_exists(__DIR__.'/'.$path)` 在迁云后必然为假 → **整个图库区块不渲染**(比 404 更隐蔽,因为页面上什么都没有);`'/'.$path` 则会拼出一个不存在的站内路径 → 404。
|
|
|
|
|
|
→ 解法:所有地方统一走门面 `storageExists()` / `storageUrl()`。**只要绕过门面直接拼路径,迁云后就一定会出问题**,改动时务必全局搜一遍。
|
|
|
|
|
|
|
|
|
|
|
|
#### 坑 3 · 防盗链修好了,裁剪还是不能用
|
|
|
|
|
|
**裁剪要的是 CORS,不是 Referer。** 两者是不同的开关。
|
|
|
|
|
|
→ 见 §3③(桶 CORS + `<img crossorigin>`);或者直接切服务端代理。
|
|
|
|
|
|
|
|
|
|
|
|
#### 坑 4 · 开了服务端代理后「慢到无法接受」
|
|
|
|
|
|
**内网 endpoint 跨地域**:服务器在 `cn-qingdao`、桶在 `cn-beijing`,程序却按桶的地域推导出 `oss-cn-beijing-internal.aliyuncs.com` 并「内网优先」,于是**每张图都先白等满一次连接超时(默认 10 秒)**才回退公网。
|
|
|
|
|
|
**这个地址的 DNS 是能解析的**(0.001 秒返回 A 记录),所以从日志上看不出任何异常,只会看到「很慢」。
|
|
|
|
|
|
→ 解法见 §4「内网 endpoint 只在与桶同地域时可用」。本仓库已改为「先比地域、结论缓存、内网只等 2 秒」。
|
|
|
|
|
|
|
|
|
|
|
|
#### 坑 5 · 图片数量越多越慢,慢得不成比例
|
|
|
|
|
|
**会话锁**。代理页都要登录,`session_start()` 在请求开头;PHP 的会话文件锁是**排他**的,若持有到脚本结束(而取存储的耗时操作就在锁内),浏览器为同源图片同时开的约 6 条连接会**全部卡在同一把锁上**——总耗时 ≈ 图片数 × 单张耗时。
|
|
|
|
|
|
→ 解法:鉴权通过后立刻 `session_write_close()`。本仓库 `media.php` 已处理。
|
|
|
|
|
|
|
|
|
|
|
|
#### 坑 6 · 控制台刷满「图片解析失败」,但服务端一切正常
|
|
|
|
|
|
代理页未登录时返回了 **302 跳登录页**,`<img>` 拿到的是 HTML。
|
|
|
|
|
|
→ 解法:图片接口未授权要返回 **403 纯文本**;前端再加一层坏图占位兜底。
|
|
|
|
|
|
|
|
|
|
|
|
#### 坑 7 · 想当然地平铺上游的 `Content-Type`,等于开了同源 XSS
|
|
|
|
|
|
桶里若被放进 `.html` / `.svg`,并且你把它返回的类型原样透传,浏览器会以 `text/html` 在**本站域名下**执行它。
|
|
|
|
|
|
→ 解法:**只按扩展名白名单**决定响应类型,并加 `X-Content-Type-Options: nosniff`。
|
|
|
|
|
|
|
|
|
|
|
|
#### 坑 8 · `?k=../config.php` 能读到任意文件
|
|
|
|
|
|
对象键没有校验,`k` 参数直接被拼进存储请求。
|
|
|
|
|
|
→ 解法:严格白名单(限定目录前缀 + 字符集 + 拒绝 `..`)。
|
|
|
|
|
|
|
|
|
|
|
|
#### 坑 9 · JSON 接口突然解析失败,报 `Unexpected token '<'`
|
|
|
|
|
|
`display_errors` 开着时,PHP 的 **Warning** 会以 HTML 形式打在 JSON 前面。触发源常常是宝塔等环境注入的 `.user.ini`:`open_basedir=/站点目录/:/tmp/`,此时对 `/etc/ssl/…` 这类**站外路径**调用 `is_file()` 会抛 `open_basedir restriction in effect` 警告。
|
|
|
|
|
|
→ 解法:探测系统文件之前**自己先判断 `open_basedir` 允不允许**(`storagePathAllowed()`),并且所有探测加 `@` 抑制。生产环境还应关闭 `display_errors`。
|
|
|
|
|
|
|
|
|
|
|
|
#### 坑 10 · 用命令行排查时「图片根本不在云上」——假象
|
|
|
|
|
|
`db/.settings.key` 的属主是 Web 用户(如 `www`,权限 600)。换一个用户跑 `php` 或 PHP 脚本,**解密失败** → 读出的云存储配置是空的 → 门面回退本地 → 表现为「404、耗时 0 秒」,很容易误判成「对象没上传成功」。
|
|
|
|
|
|
→ 解法:**必须以 Web 用户身份**跑诊断代码(临时放一个带随机 token 门禁、用完即删的诊断页,或用 Web 用户执行 CLI)。
|
|
|
|
|
|
|
|
|
|
|
|
#### 坑 11 · 桶名看错了一个字母,排查了半天
|
|
|
|
|
|
Bucket 名称是全局唯一标识,**字母一个都不能错**,而拼错时通常不会给出「你拼错了」这种提示,只会得到 404 / 403(阿里云会回 `NoSuchBucket`)。
|
|
|
|
|
|
→ 在响应体里找 `<BucketName>`,或直接以控制台实际显示为准,别凭记忆填写。
|
|
|
|
|
|
|
|
|
|
|
|
#### 坑 12 · 部署脚本「备份成功」但实际没备份
|
|
|
|
|
|
`cp -p` 多级路径(`lib/storage/oss.php`)到备份目录时,**子目录不存在会静默失败**,脚本继续往下走覆盖了线上文件,事后才发现没有可回滚的副本。
|
|
|
|
|
|
→ 解法:备份循环里先 `mkdir -p "$BK/$(dirname "$f")"`;备份后**递归统计文件数并与预期条数比对**,数量不符立即中止(这是覆盖前的最后一道止损闸);覆盖后再逐文件比对 sha256。
|
|
|
|
|
|
|
|
|
|
|
|
### 7. 排障手册(可直接复制的命令)
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# ① 判断是不是防盗链:A / B 对比(把 URL 换成你的)
|
|
|
|
|
|
URL="https://<bucket>.oss-cn-xxx.aliyuncs.com/movielog/uploads/posters/xxx.webp"
|
|
|
|
|
|
curl -sS -o /dev/null -w "无 Referer %{http_code}\n" "$URL"
|
|
|
|
|
|
curl -sS -o /dev/null -w "带本站 Referer %{http_code}\n" -H "Referer: https://你的域名/" "$URL"
|
|
|
|
|
|
curl -sS -o /dev/null -w "带陌生 Referer %{http_code}\n" -H "Referer: https://evil.example.com/" "$URL"
|
|
|
|
|
|
# 403 时把响应体打出来,里面有云厂商的原始判决
|
|
|
|
|
|
curl -sS -H "Referer: https://你的域名/" "$URL"
|
|
|
|
|
|
|
|
|
|
|
|
# ② 是否配了 CORS(裁剪用)
|
|
|
|
|
|
curl -sS -D - -o /dev/null -H "Origin: https://你的域名" "$URL" | grep -i "access-control"
|
|
|
|
|
|
|
|
|
|
|
|
# ③ 在服务器上量内外网可达性与耗时(诊断「代理很慢」第一步)
|
|
|
|
|
|
curl -o /dev/null -s -w 'dns=%{time_namelookup} conn=%{time_connect} tls=%{time_appconnect} total=%{time_total} code=%{http_code}\n' \
|
|
|
|
|
|
--max-time 20 https://<bucket>.oss-cn-xxx-internal.aliyuncs.com/
|
|
|
|
|
|
curl -o /dev/null -s -w 'public conn=%{time_connect} tls=%{time_appconnect} total=%{time_total} code=%{http_code}\n' \
|
|
|
|
|
|
--max-time 20 https://<bucket>.oss-cn-xxx.aliyuncs.com/
|
|
|
|
|
|
|
|
|
|
|
|
# ④ 本机 ECS 地域(与桶地域对比,不同就别走内网)
|
|
|
|
|
|
curl -s --max-time 5 http://100.100.100.200/latest/meta-data/region-id
|
|
|
|
|
|
|
|
|
|
|
|
# ⑤ 端到端计时:在**服务器本机**打自己的站点,排除公网 RTT
|
|
|
|
|
|
# (Cookie 用浏览器里已登录的 PHPSESSID)
|
|
|
|
|
|
curl -sk -H "Host: 你的域名" -H "Cookie: PHPSESSID=xxxx" -o /dev/null \
|
|
|
|
|
|
-w "ttfb=%{time_starttransfer} total=%{time_total} size=%{size_download}\n" \
|
|
|
|
|
|
"https://127.0.0.1/media.php?k=uploads/posters/xxx.webp"
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**读结果的方法**:`time_namelookup` 很快但 `time_connect` 拿不到 → 目标不可路由(典型就是跨地域内网);
|
|
|
|
|
|
单张图服务端耗时应当在**几百毫秒内**;若接近 10 秒、或总耗时随图片数量线性增长,回去看坑 4 与坑 5。
|
|
|
|
|
|
|
|
|
|
|
|
### 8. 部署与运维注意
|
|
|
|
|
|
|
|
|
|
|
|
- **改桶权限前先确认代码已上线**:直连↔代理的切换顺序建议「先部署代码 → 切访问方式 → 确认图片正常 → 最后才改桶 ACL / 防盗链」,每一步都可单独回滚。
|
|
|
|
|
|
- **代理模式会占服务器出网带宽**:图片流量走一遍服务器。用「阿里云内网 endpoint + 同地域 ECS」可以免掉云端到服务器的流量费;跨地域则要计费。
|
|
|
|
|
|
- **`uploads/` 与 `db/` 不要暴露**:见《安全注意事项》。
|
|
|
|
|
|
- **迁移/删除本地文件是有损操作**:先备份 `db/` 与 `uploads/`,再动手。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-09-21 08:20:56 +08:00
|
|
|
|
## 安全注意事项
|
|
|
|
|
|
|
|
|
|
|
|
部署到公网前请务必处理以下事项:
|
|
|
|
|
|
|
|
|
|
|
|
1. **修改默认口令**:默认账号 `admin / 123456` 必须在首次登录后立即更换。
|
|
|
|
|
|
2. **更换 OMDb API Key**:`fetch_movie.php` 中当前硬编码的 Key 为开发期临时使用,请替换为你自己的 Key;若仓库需要公开,建议将该值改为从环境变量读取。
|
|
|
|
|
|
3. **删除或保护 `test_crop.php`**:该文件是开发调试页,**没有登录校验**,仅用于验证 Base64 图片能否落盘,生产环境应直接删除。
|
|
|
|
|
|
4. **关闭调试输出**:`config.php` 顶部当前为 `error_reporting(E_ALL)` + `display_errors=1`,上线前请改为关闭或仅写日志,避免泄露路径与数据库信息。
|
|
|
|
|
|
5. **启用 HTTPS 并校正证书校验**:`fetch_movie.php` 中 `CURLOPT_SSL_VERIFYPEER` 被设为 `false`,生产环境应改为 `true`。
|
|
|
|
|
|
6. **保护数据目录**:`db/`(含 `movies.db`)与 `uploads/` 应禁止直接目录列举;SQLite 文件更应通过 Web 服务器规则**拒绝任何 HTTP 访问**。Nginx 示例:
|
|
|
|
|
|
```nginx
|
|
|
|
|
|
location ~ ^/(db|uploads/.*\.(db|sqlite)) { deny all; }
|
|
|
|
|
|
```
|
|
|
|
|
|
7. **限制上传目录执行权限**:确保 `uploads/` 下不允许执行 PHP。
|
|
|
|
|
|
|
2026-09-21 22:06:02 +08:00
|
|
|
|
### 与对象存储相关的额外事项
|
|
|
|
|
|
|
|
|
|
|
|
8. **桶不要长期开公共读**:直连存储要求桶允许匿名读取,等于**任何拿到 URL 的人都能看图**。私人 / 家庭内容请改用「服务端代理」,把桶设为**私有**——那才是真正的访问控制。
|
|
|
|
|
|
9. **防盗链不是访问控制**:`Referer` 是客户端可伪造的普通请求头,`curl -H Referer` 就能绕过。它只能挡住「别的网站盗链」,不能保护内容。别把它当成安全边界。
|
|
|
|
|
|
10. **保护好 `db/.settings.key`**:云厂商 AccessKey 以密文存在数据库里,这个文件是唯一的解密密钥。应随 `db/` 一起纳入访问控制与备份策略;优先改用实例角色(RAM / CAM 角色)**免密钥**方案,从根上不落长期密钥。
|
|
|
|
|
|
11. **`media.php` 是新增的对外接口**:它按 `k` 参数从对象存储取对象。仓库里已做**对象键白名单**(防目录穿越)与**类型白名单**(防同源 XSS)两层校验,改动此文件时**务必保留**,不要把 `$_GET['k']` 直接拼进存储请求。
|
|
|
|
|
|
12. **不要把上游 `Content-Type` 原样透传**:桶里若有 `.html` / `.svg`,透传类型会让它在本站域名下执行(同源 XSS)。始终按扩展名白名单决定,并加 `X-Content-Type-Options: nosniff`。
|
|
|
|
|
|
|
2026-09-21 08:20:56 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 已知限制与待改进
|
|
|
|
|
|
|
|
|
|
|
|
- `movies.photo_path` 与 `viewers.name` 是历史遗留字段,保留用于兼容旧数据与 CSV 导入,新代码统一使用 `movie_photos` 与 `person_id`。
|
|
|
|
|
|
- 除 `delete.php` 通过 GET 触发(带前端 `confirm`)外,其余写操作均为 POST;删除类接口未做 CSRF Token 校验。
|
|
|
|
|
|
- 暂无分页,数据量增大后首页与统计页需要补充分页或懒加载。
|
|
|
|
|
|
- `config.php` 的 `initDB()` 在每次请求都会跑一遍建表与迁移,数据量或并发上升后可改为一次性迁移脚本。
|
|
|
|
|
|
- 无「回收站」,删除电影与其图片文件不可恢复。
|
2026-09-21 22:21:01 +08:00
|
|
|
|
- `assets/placeholder-poster.jpg` 仍是代码中的兜底引用,**仓库未包含该文件**;页面在检测到 placeholder 时会改用 CSS 占位块渲染,正常使用不受影响。`assets/default-avatar.png` 已随仓库提供(默认头像),并在资源缺失时由 `getAvatarUrl()` 退化为内联 SVG 剪影。
|
2026-09-21 22:06:02 +08:00
|
|
|
|
- **「直连存储」模式下裁剪功能不可用**:裁剪需要读取图片像素,直连时图片来自对象存储域名,必须同时满足「桶配 CORS」与「`<img crossorigin="anonymous">`」两个条件才能读像素;本仓库未加 `crossorigin` 属性,因此直连模式下只能上传、不能二次裁剪。需要裁剪请改用「服务端代理」(同源,两个条件全免)。详见《图片存储与对象存储》§3③。
|
|
|
|
|
|
- **服务端代理模式下所有图片流量经服务器中转**,会占用服务器出网带宽,并给每张图增加一次服务器↔存储的往返(同地域约 10–30 ms,跨地域约 60–100 ms)。图片量大且可公开时可考虑改回直连。
|
|
|
|
|
|
- **内网 endpoint 只在服务器与桶同地域时生效**。跨地域部署下这条免费通道用不了,程序会自动跳过并走公网(详见 §4)。
|
|
|
|
|
|
- 迁移到云端后**本地文件不会自动删除**,需要自行确认后再清理;本地优先策略意味着本地文件存在时始终读本地,云端副本仅作备份。
|
2026-09-21 08:20:56 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 版本控制说明
|
|
|
|
|
|
|
|
|
|
|
|
以下内容为运行时产物或敏感数据,**不纳入版本控制**(见 `.gitignore`):
|
|
|
|
|
|
|
|
|
|
|
|
- `db/`(SQLite 数据库文件)
|
|
|
|
|
|
- `uploads/`(用户上传的图片)
|
|
|
|
|
|
- `*.log`、编辑器与系统临时文件
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 许可
|
|
|
|
|
|
|
|
|
|
|
|
个人项目,未指定开源许可。
|