# MovieLog_Server · 电影全纪录 一个轻量级的个人/家庭观影记录系统。纯 PHP 编写,使用 SQLite 作为数据库,**无需任何框架、无需 Composer、无需配置数据库服务**,上传到支持 PHP 的空间即可运行。 > 记录每一帧感动 —— 观影记录、影院、同行人、票根与照片,以及属于你的年度观影报告。 --- ## 功能特性 ### 观影记录管理 - **瀑布流首页**:按观影日期倒序展示所有电影,卡片显示海报、日期、影院(影院专属配色)、同行人头像。 - **添加电影**:支持电影名、剧情简介、观影日期、电影院、同行人、海报、票根、多张观影照片。 - **编辑 / 删除**:编辑页可替换或裁剪已有图片;删除电影时同步清理关联的图片文件。 - **电影详情**:海报大图、基本信息、备注、同行人列表(含头像)、影院信息、影像记录画廊(票根 + 观影照片,点击可查看原图)。 ### 图片处理 - **内置裁剪器**:基于 Cropper.js,票根 / 海报 / 观影照片 / 观影人头像均可在浏览器内裁剪后再保存,服务端接收 Base64 图片落盘。 - **多图上传**:观影照片支持一次选择多张。 - **头像兜底**:未上传头像的观影人一律显示 `assets/default-avatar.png` 默认头像(人形剪影);若该资源缺失(例如只部署了 PHP 而漏了 `assets/`),`getAvatarUrl()` 会退化为内联 SVG 剪影 —— 保证任何情况下都不会出现"坏图"。 - 上传目录按类型分离:`uploads/posters/`、`uploads/tickets/`、`uploads/photos/`、`uploads/avatars/`。 - **破图兜底**:图片加载失败时由前端脚本替换为内联占位图,避免会话过期或权限异常时满屏破图刷屏控制台。 ### 图片存储(本地 / 云对象存储) - **四种存储后端**:本地硬盘(默认)、阿里云 OSS、腾讯云 COS、七牛云 Kodo,在「系统设置」页切换,**无需改代码**。 - **免密钥鉴权**:部署在阿里云 ECS / 腾讯云 CVM 上时,可直接绑定 RAM/CAM 角色读取实例元数据获取临时凭据,**不必在站内保存长期密钥**。 - **本地优先读取**:数据库里只存相对对象键(如 `uploads/posters/xxx.webp`);本地存在就本地读,否则读云端。**因此切换存储方式不会影响已有图片**,迁移可以慢慢来。 - **内容指纹命名**:文件名为 `md5(内容)前 12 位_6 位随机[_角色].ext`,内容不变则名字不变,可以放心下长缓存。 - **一键迁移**:把已有本地图片上传到云端并自动改写数据库引用(分批执行、带进度、幂等可重入)。 - **两种图片访问方式**:**直连存储**(浏览器直接访问对象存储)与**服务端代理**(经本站 `media.php` 鉴权后由服务器读取回吐,**桶可保持私有**)。两者的取舍与配置见下文《图片存储与对象存储》。 ### 信息辅助 - **豆瓣自动获取**:在添加页输入片名时会**实时下拉联想候选**(带海报缩略图、年份、电影/剧集标注),**不必把片名打全**——从候选里点一条即可;也可以直接按回车或点「📡 自动获取」由服务端挑最匹配的条目。自动填充年份、地区、类型、片长、导演、主演、豆瓣评分、又名与剧情简介,**并把海报下载入库**——提交时若未手动上传图片,就直接采用自动获取的海报。 - **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 | [豆瓣](https://movie.douban.com/)(公开网页接口,**无需申请 Key**,用于电影信息与海报查询) | --- ## 目录结构 ``` MovieLog_Server/ ├── config.php # 引导入口:错误报告、会话、路径常量、上传目录,并加载 lib/ 各模块 ├── lib/ # 功能模块(被 config.php 统一引入,页面无需单独 require) │ ├── db.php # DB 连接、建表、字段与历史数据迁移、默认管理员 │ ├── helpers.php # 输出转义、文件名、颜色校验、上传/删除文件、图片 URL │ ├── 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 角色)与本机地域查询 │ ├── auth.php # 登录校验、管理员账号增删改 │ ├── movies.php # 电影/影院读取、搜索,以及表单关联数据的解析 │ └── stats.php # 统计仪表板与年度报告查询 ├── 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) ├── settings.php # 系统设置页:存储后端选择、图片访问方式、连接测试、迁移 ├── storage_api.php # 存储维护接口(stat / migrate / test),返回 JSON ├── media.php # 服务端图片代理:鉴权后由服务器读取对象并同源回吐 ├── fetch_movie.php # 豆瓣接口(片名候选 / 详情 + 海报入库 / 缩略图代理),返回 JSON ├── test_crop.php # 开发调试页:验证 Base64 图片落盘是否正常 ├── css/style.css # 全局样式 ├── js/main.js # 公共脚本(含各页面共用的表单联动函数、破图兜底) ├── assets/ # 静态资源 │ └── default-avatar.png # 默认头像(未上传头像时统一使用) ├── db/movies.db # SQLite 数据库(运行时自动生成,不纳入版本控制) └── uploads/ # 上传图片(运行时自动生成,不纳入版本控制) ├── posters/ # 海报 ├── tickets/ # 票根 ├── photos/ # 观影照片 └── avatars/ # 观影人头像 ``` > 页面只需 `require_once 'config.php'`,`lib/` 下的模块会由引导文件统一加载,并保证依赖顺序(db → helpers → auth → movies → stats)。 --- ## 数据库结构 建表逻辑集中在 `lib/db.php` 的 `initDB()`(由 `config.php` 在每次请求引导时调用),每次请求都会执行 `CREATE TABLE IF NOT EXISTS` 与幂等的 `ALTER TABLE` 迁移,因此**升级代码无需手动改库**。 | 表 | 作用 | 关键字段 | | --- | --- | --- | | `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` | | `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 分钟 | 外键约束:`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 内置服务器均可 - **使用对象存储时额外需要**: - `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` 首次保存时在此生成)。 ### 步骤 1. **放置代码**:将项目文件放到 Web 根目录或其子目录。 2. **确保可写**:保证项目根目录可写,程序会自动创建 `db/` 与 `uploads/` 及其子目录;或手动创建并授权: ```bash mkdir -p db uploads/posters uploads/tickets uploads/photos uploads/avatars chmod -R 755 db uploads ``` 3. **确认「自动获取」可用(可选)**:该功能由服务端直接请求豆瓣的公开网页接口实现,**无需申请任何 API Key**。只需保证: - PHP 已启用 `curl` 扩展(本项目的图片下载、对象存储也都依赖它); - 服务器能出网访问 `movie.douban.com`、`m.douban.com` 与 `img*.doubanio.com`。 若服务器网络受限导致请求失败,「自动获取」会返回明确提示,但**不影响其它功能**——照常手动填写信息、上传海报即可。 4. **访问站点**:浏览器打开 `login.php`。 5. **首次登录**:数据库为空时会自动创建默认管理员: - 用户名 `admin` - 密码 `123456` > **登录后请立即在「修改密码」中更换。** ### 本地快速运行 ```bash php -S localhost:8000 # 浏览器访问 http://localhost:8000/login.php ``` > 用 PHP 内置服务器时,`posterUrl()`(`lib/helpers.php`)与 `storageUrl()`(`lib/storage.php`)返回的是以 `/` 开头的站点绝对路径(如 `/uploads/posters/xxx.webp`),建议从项目根目录启动,或通过站点根目录访问。 ### 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) ``` ### 把图片放到对象存储(可选) 不改也能用(默认本地硬盘)。想用云存储时: 1. 进入**系统设置**,选择存储后端(阿里云 OSS / 腾讯云 COS / 七牛云 Kodo),填写 Bucket、Endpoint、对象前缀等。 2. 选择鉴权方式: - **实例角色(免密钥,推荐)**:服务器在对应云上并绑定了角色时选它,不用在站内保存任何长期密钥。 - **AccessKey**:手动填写密钥,入库即加密。 3. 点**测试连通性**——它会执行「写入探针 → **读回校验** → 删除探针」,因此**同时验证了写和读两条链路**,并会明确报出内网 endpoint 的判定结论。 4. 选择**图片访问方式**(直连存储 / 服务端代理),两个选项的取舍见《图片存储与对象存储》§2。 5. 需要的话点**迁移**,把已有本地图片搬到云端(分批、可重入,建议先备份 `db/` 与 `uploads/`)。 6. 迁移完成后**全站看图确认无误,再清理本地文件**——读取是本地优先的。 ### 接口:`fetch_movie.php` 供添加页前端调用,服务端代请求豆瓣(既避免浏览器跨域,也复用服务器的出网能力)。 共 **4 种模式**,全部**需要登录**(未登录一律返回 `HTTP 401` + `{"error": "…"}`): | 模式 | 请求 | 作用 | 是否写文件 | | --- | --- | --- | --- | | 1. 按片名抓取 | `?q=片名[&old_poster=键]` | 搜索 → 取详情 → 下载海报入库 | 是(写海报) | | 2. 按条目 id 抓取 | `?id=豆瓣条目id[&old_poster=键]` | 跳过搜索,直接取详情 | 是(写海报) | | 3. 下拉候选 | `?action=suggest&q=关键词` | 只回候选列表 | **否** | | 4. 缩略图代理 | `?action=thumb&u=图片地址` | 代取豆瓣缩略图字节 | **否** | **模式 1 / 2**(抓取详情并把海报入库) - **参数** - `q`:片名,中文或英文均可(英文名会命中对应的中文条目) - `id`:豆瓣条目 id(纯数字)。下拉选中后走这条路,**省一次搜索,也不会「再搜一次选错条目」** - `old_poster`:可选。用于清理「上一次抓取、但本次未采用」的海报文件,前端在重复获取时自动回传 - **成功响应**: ```json { "ok": true, "source": "douban", "douban_id": "34780991", "douban_url": "https://movie.douban.com/subject/34780991/", "title": "哪吒之魔童闹海", "original_title": "", "year": "2025", "genre": "剧情 / 喜剧 / 动画", "runtime": "144分钟", "director": "饺子", "actors": "吕艳婷 / 囧森瑟夫 / 瀚墨", "plot": "……剧情简介……", "rating": "8.4", "country": "中国大陆", "alias": "哪吒2 / 哪吒2之魔童闹海 / Ne Zha 2", "poster": "/uploads/posters/xxxxxxxxxxxx_xxxxxx_poster.jpg", "poster_key": "uploads/posters/xxxxxxxxxxxx_xxxxxx_poster.jpg" } ``` - **字段说明**: - `poster` 是给 `` 预览的地址。海报已由本接口**下载入库**,所以它是本站同源地址(或存储直链),不是豆瓣的外链。 - `poster_key` 是提交表单时应写入 `movies.poster_path` 的存储键。前端把它放进隐藏域 `fetched_poster_key` 回传; 提交时**手动上传的海报优先**,没有才采用该键。键的目录与文件名、以及文件是否真实存在,都由服务端校验。 - `rating` 为豆瓣评分(保留一位小数)。无评分、或该片暂无海报时,对应字段为空串。 **模式 3**(片名下拉联想的候选列表;**不取详情、不写任何文件**,可随输入频繁调用) - **请求**:`GET fetch_movie.php?action=suggest&q=关键词` - **成功响应**: ```json { "ok": true, "items": [ { "id": "34780991", "title": "哪吒之魔童闹海", "sub_title": "哪吒之魔童闹海", "year": "2025", "episode": "", "kind": "电影", "thumb": "fetch_movie.php?action=thumb&u=https%3A%2F%2Fimg9.doubanio.com%2F..." } ] } ``` - **说明**: - `q` 为空时直接返回 `{"ok": true, "items": []}`,**不会请求上游**;上游无结果时同样返回空数组(不算错误) - 会过滤掉「影人」类条目、按条目 id 去重,默认**最多 8 条** - `kind` 由豆瓣的 `episode`(集数)推断:有集数即「剧集」,否则「电影」 - `thumb` 是**本站代理地址**(见模式 4),不是豆瓣外链;该候选没有海报时为空串 - **失败**:上游请求失败时为 `{"error": "搜索失败:…"}` **模式 4**(缩略图服务端代取) - **请求**:`GET fetch_movie.php?action=thumb&u=<图片地址>`(`u` 需 URL 编码) - **成功**:直接回吐图片字节,带 `Content-Type: image/*` 与 `Cache-Control: public, max-age=86400` - **为什么要代理**:豆瓣图片有**防盗链**——只有 `Referer` 为豆瓣域名时才返回 200,其它一律 403。 浏览器直连(本站 `Referrer-Policy: same-origin`,跨域图片不带 Referer)必然被拒,所以只能由服务端带 Referer 代取。 - **安全**:该地址做了**严格白名单**(只允许 `https://img*.doubanio.com/view/photo/…jpg|png|webp`), 否则就成了 SSRF 跳板。非白名单地址返回 `HTTP 400`;目标不是图片返回 `HTTP 404`。 - **失败**:`400`(地址不合法)/ `404`(取不到或非图片) **模式 1 / 2 的失败响应**:`{"error": "错误说明"}`(如「豆瓣没有找到「xxx」,换个更准确的片名试试」);未知 `action` 返回 `{"error": "不支持的 action:…"}`。 > **关于数据来源与合规**:本功能使用的是豆瓣的**公开网页接口**(非官方开放 API,也不需要 API Key), > 按需抓取少量条目用于个人观影记录,并已加入登录鉴权。请自行评估使用场景与豆瓣的服务条款; > 若需商用或大规模采集,请改用官方或商业数据源。 --- ## 图片存储与对象存储(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://.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 放在页面 `` 里 → **403**。 原因是浏览器对子资源(``/`