🥹(U+1F979,含泪微笑)是 Emoji 14.0 才新增的码位(2021-09), 较旧的系统 emoji 字体里没有这个字形——例如本机 Segoe UI Emoji 的 cmap 里就没有 U+1F979,Windows 上页面的「感动」图标就显示成空白。 其余 11 个感受用的都是 Emoji 1.0~11.0(2010–2018)的老码位,显示正常。 现换成 💖(U+1F496,Emoji 1.0 / 2010),各平台字体都有字形。 感受在库里只存 key(moved),emoji 与文案统一在 feelingCatalog() 里, 所以这是纯显示层变更:不需要数据迁移,已有的评价完全不受影响。 同时补了一条约定注释:以后加新感受只用 Emoji 11.0(2018)及以前的 老码位,并在注释里一律写码位而不写 emoji 字符本身。 - lib/reviews.php feelingCatalog() 中 moved 的 emoji;新增约定注释 - README.md 同步感受清单里的图标
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加盐哈希存储口令,全站页面均需登录。 - 用户管理:可新增、编辑、删除管理员账号,可修改用户名与密码。
- 账号 ↔ 观影人绑定:每个管理员账号可关联一个观影人,绑定后即可查看个人年度报告。
- 安全约束:不允许删除最后一个管理员,也不允许删除自己。
- 个人资料:修改自己的用户名与密码(需验证旧密码)。
评价与讨论
- 谁可以评价:只要有登录账号就能评价,与「是不是观影人」无关——没一起去看过的人也能打分、写评价、参与讨论。评价的归属是账号(
admins),不是观影人(persons)。 - 打分 1-10:详情页「💬 评价与讨论」区块里点数字即可打分,右侧实时显示「7 分 · 不错」;打分用的是原生单选框,禁用 JS 也能照常提交。
- 写评价:一段正文(最多 5000 字)。每个账号对每部电影只有一条评价,再次提交即修改,页面上会标注「已编辑」。
- 讨论:任意账号都能在任意一条评价下面参与讨论(正文最多 1000 字)。
- 观后感受(可多选):写评价时和发讨论时都可以勾选自己的观后感受,每种感受一个专属 emoji:😄 开心、💖 感动、🥰 温暖、🤩 激动、🤔 深思、😰 紧张、😨 恐惧、😲 震惊、😢 难受、😠 气愤、😴 无聊、😵 困惑。勾选多少个都行(最多 6 个),展示时按固定顺序排列。
- 评价上的感受会显示在这条评价正文下面;修改评价时原来勾的会自动带上,取消勾选即清空。
- 感受只把 key(如
happy)存进数据库(reviews.feelings/review_comments.feelings),emoji 与文案统一在lib/reviews.php的feelingCatalog()里维护,以后换 emoji / 改文案都不用动数据。
- 评分概况:详情页标题栏显示「N 条评价 · 均分 X」;首页列表卡片上有评价的电影会显示
⭐ 均分。 - 删除:只能删除自己的评价与讨论;删除评价时其下的讨论一并删除。删除电影时也会清掉对应的评价与讨论(SQLite 默认未开启外键级联,由程序手动清理)。
基础数据维护
- 影院管理:增删改查,支持「所在地方 / 影院名称 / 详细地址」三段式信息,内置 50 种预设配色,每个影院可用专属颜色区分;删除已被引用的影院时会给出醒目提示并要求二次确认(关联电影的影院字段将被置空)。
- 观影人管理:增删改查,支持头像裁剪上传与专属配色;已被电影引用的观影人不允许直接删除。
界面与体验
- 复古票根色系(主色
#e67e22橙)的响应式设计,移动端断点适配(600 / 768px)。 - 导航为下拉菜单,自动高亮当前页;顶部始终显示当前用户头像与用户名。
- 点导航栏左上角的「🎬 电影日志」即可回到首页(右键 / Ctrl+点击还能在新标签页打开),不必再展开右侧折叠菜单去找「首页」。
- 登录页为深色渐变 + 毛玻璃风格。
- 详情页图片用本页灯箱查看:海报、票根、观影照片串成一个序列,点任一张即在当前页弹出(不再
window.open到新窗口),左右箭头翻看并循环,底部显示「3 / 8」进度与图片说明。 可按 ← / → 翻页、Esc 关闭、点画面空白处关闭,手机上还能左右滑动切换;弹出时锁定背景滚动,并从相邻位置预取下一张,翻页不白屏。 只有一张图时不显示左右箭头;关闭后焦点会回到原来点开的那张图上。
技术栈
| 项目 | 说明 |
|---|---|
| 语言 | PHP(过程式 + PDO) |
| 数据库 | SQLite(db/movies.db,首次运行自动建表) |
| 前端 | 原生 HTML / CSS / JavaScript,无构建步骤 |
| 第三方库 | Cropper.js 1.5.13(图片裁剪)、Chart.js 4.4.0(图表),均由 CDN 引入 |
| 外部 API | 豆瓣(公开网页接口,无需申请 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 # 电影/影院读取、搜索,以及表单关联数据的解析
│ ├── reviews.php # 评价与讨论:打分 1-10、正文、讨论、观后感受目录(emoji)
│ └── 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 → settings → storage → helpers → auth → movies → reviews → 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 |
reviews |
电影评价 | movie_id、admin_id(评价人是账号,不是观影人)、rating(1-10)、content、feelings(写评价时选的观后感受,同样是 JSON 数组)、created_at、updated_at;(movie_id, admin_id) 上有唯一索引,保证一个账号一部电影只一条 |
review_comments |
评价下的讨论 | review_id、admin_id、content、feelings(感受 key 的 JSON 数组,如 ["happy","sad"])、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)、reviews.movie_id → movies.id(ON DELETE CASCADE)、review_comments.review_id → reviews.id(ON DELETE CASCADE)。
SQLite 默认是关闭外键约束的(
PRAGMA foreign_keys未开启),上面这些ON DELETE CASCADE实际不会生效。因此删除电影、删除评价时,程序都会显式地把子表数据一并清掉(见delete.php与lib/reviews.php),不要依赖级联。
历史数据迁移: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首次保存时在此生成)。
步骤
-
放置代码:将项目文件放到 Web 根目录或其子目录。
-
确保可写:保证项目根目录可写,程序会自动创建
db/与uploads/及其子目录;或手动创建并授权:mkdir -p db uploads/posters uploads/tickets uploads/photos uploads/avatars chmod -R 755 db uploads -
确认「自动获取」可用(可选):该功能由服务端直接请求豆瓣的公开网页接口实现,无需申请任何 API Key。只需保证:
- PHP 已启用
curl扩展(本项目的图片下载、对象存储也都依赖它); - 服务器能出网访问
movie.douban.com、m.douban.com与img*.doubanio.com。
若服务器网络受限导致请求失败,「自动获取」会返回明确提示,但不影响其它功能——照常手动填写信息、上传海报即可。
- PHP 已启用
-
访问站点:浏览器打开
login.php。 -
首次登录:数据库为空时会自动创建默认管理员:
- 用户名
admin - 密码
123456
登录后请立即在「修改密码」中更换。
- 用户名
本地快速运行
php -S localhost:8000
# 浏览器访问 http://localhost:8000/login.php
用 PHP 内置服务器时,
posterUrl()(lib/helpers.php)与storageUrl()(lib/storage.php)返回的是以/开头的站点绝对路径(如/uploads/posters/xxx.webp),建议从项目根目录启动,或通过站点根目录访问。
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;
}
}
使用说明
添加一部电影
- 顶部导航点击「➕ 添加电影」。
- 在「电影名称」里输入片名的前几个字,下方会实时弹出候选列表(海报缩略图 + 片名 + 原名 + 年份 + 电影/剧集)。可以用 ↑ / ↓ 选择、回车确认,也可以直接用鼠标点选——不必把片名打全,选中后会自动填充影片信息。若候选里没有想要的条目,直接点「📡 自动获取」(或按回车)由服务端挑最匹配的条目(可选步骤)。
- 填写观影日期(必填)、选择或新建电影院。
- 「一起看的人」逐行添加:选择已有观影人,或在下拉框中选「➕ 新增」并输入姓名,新姓名会自动进入观影人库。
- 上传海报(推荐 2:3);上传票根会弹出裁剪框,裁剪结果以 Base64 提交。
- 观影照片支持一次多选。
- 保存后自动跳转到详情页。
生成年度报告
年度报告是按人生成的,需要先建立账号与观影人的绑定:
- 进入「用户管理」,找到自己的账号,在「关联观影人」中选择对应的观影人并保存。
- 进入「年度报告」,页面顶部切换年份即可查看。
- 未绑定观影人时报告为空并给出提示。
打分、写评价与参与讨论
- 打开任意一部电影的详情页,滚到最下面的「💬 评价与讨论」。
- 打分:点「我的评分」后面的 1-10 任意一个数字(选中会变橙色),旁边实时显示「几分 · 怎么样」。
- 写评价:在下面文本框里写正文;再到「我看完的感受(可多选)」里勾几个感受 emoji,点「📝 发表评价」。
- 已经评价过时,这一块会变成「✏️ 修改我的评价」,按钮是「💾 保存修改」——同一部电影不会留下两条你的评价,原来勾的感受会自动带上,取消勾选就是清掉。
- 评价保存后,感受会以只读小标签显示在这条评价正文下面。
- 参与讨论:在别人的(或自己的)任意一条评价下面,写一句话,同样可以勾选感受,点「💬 发表讨论」。
- 讨论会带上评价人的头像、昵称,以及你选的感受 emoji。
- 删除:自己发的评价和讨论,右侧有删除按钮(别人的没有);删评价时下面的讨论会一起删掉。
评价人是登录账号。家里几个人各有一个账号时,同一部电影可以各自打分,标题栏会显示「N 条评价 · 均分 X」。
CSV 批量导入
CSV 共 9 列,顺序固定:
电影名称, 地点(忽略), 电影院名称, 观影日期, 观影人(空格分隔), 电影票图片路径, 海报图片路径, 备注, 合影图片路径
- 观影人:多个姓名用空格分隔,例如
张三 李四 王五。 - 图片路径:可带目录,系统只取最后一个
/后的文件名;若整段被括号包裹(如(电影票/阿甘正传.jpg)),会先取括号内内容再取文件名。 - 观影日期可为空,空日期的记录会排在列表最后。
- 导入前需先把图片文件放入
uploads/tickets/、uploads/posters/、uploads/photos/,文件名与 CSV 中提取出的保持一致。 - 编码支持 UTF-8 / GBK / GB2312,自动识别转换。
- 导入在事务中执行,失败会整体回滚;成功后显示「导入 N 条 / 跳过 M 条」及逐行错误原因。
示例行:
阿甘正传, , 万达影城, 2024-01-15, 张三 李四, (电影票/阿甘正传.jpg), (海报/阿甘正传.jpg), 经典励志片, (合影/阿甘正传.jpg)
把图片放到对象存储(可选)
不改也能用(默认本地硬盘)。想用云存储时:
- 进入系统设置,选择存储后端(阿里云 OSS / 腾讯云 COS / 七牛云 Kodo),填写 Bucket、Endpoint、对象前缀等。
- 选择鉴权方式:
- 实例角色(免密钥,推荐):服务器在对应云上并绑定了角色时选它,不用在站内保存任何长期密钥。
- AccessKey:手动填写密钥,入库即加密。
- 点测试连通性——它会执行「写入探针 → 读回校验 → 删除探针」,因此同时验证了写和读两条链路,并会明确报出内网 endpoint 的判定结论。
- 选择图片访问方式(直连存储 / 服务端代理),两个选项的取舍见《图片存储与对象存储》§2。
- 需要的话点迁移,把已有本地图片搬到云端(分批、可重入,建议先备份
db/与uploads/)。 - 迁移完成后全站看图确认无误,再清理本地文件——读取是本地优先的。
接口: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:可选。用于清理「上一次抓取、但本次未采用」的海报文件,前端在重复获取时自动回传
- 成功响应:
{ "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是给<img>预览的地址。海报已由本接口下载入库,所以它是本站同源地址(或存储直链),不是豆瓣的外链。poster_key是提交表单时应写入movies.poster_path的存储键。前端把它放进隐藏域fetched_poster_key回传; 提交时手动上传的海报优先,没有才采用该键。键的目录与文件名、以及文件是否真实存在,都由服务端校验。rating为豆瓣评分(保留一位小数)。无评分、或该片暂无海报时,对应字段为空串。
模式 3(片名下拉联想的候选列表;不取详情、不写任何文件,可随输入频繁调用)
- 请求:
GET fetch_movie.php?action=suggest&q=关键词 - 成功响应:
{ "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://<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 | 域名管理 → 防盗链 |
配置要点:
- 把站点域名加进白名单,例如
movie.example.com,需要子域时写*.example.com。 - 不要图省事直接关掉防盗链 —— 如果这个桶还给别的用途(比如当备份盘)用,加白名单是加法,更安全。
- 确认「允许空 Referer」是开着的(各家默认开启)。本项目
config.php里统一下发了Referrer-Policy: same-origin,即跨域请求不带 Referer、同源请求照常带,所以即使白名单里漏配了本站,页面里的图片也能正常显示。这算一层兜底,但不要拿它替代白名单。
③ 跨域 CORS:只有「裁剪 / 读像素」才需要
这不是防盗链,是另一个开关,两者经常被混为一谈。
浏览器里 cropper.getCroppedCanvas() + canvas.toDataURL() 需要读取图片的像素。如果图片来自别的域名且未获授权,Canvas 会被标记为「被污染」(tainted),读取时抛 SecurityError,表现就是「点了裁剪保存没反应 / 弹错误」。
要让直连模式下的裁剪可用,必须同时满足两件事(缺一不可):
- 桶上配置 CORS 规则:来源
https://你的域名,方法GET(HEAD建议一起),允许的请求头建议*,暴露的响应头至少ETag、Content-Length,缓存时间 600 秒。 - 图片标签声明跨域模式:
<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 却永远连不上。所以程序会:
- 先比本机 ECS 地域(实例元数据
region-id)与桶地域,不同就直接跳过内网(零成本、结论确定); - 地域相同或无法判定时,才真的发一个签名
HEAD到桶根探测一次(连接超时 2 秒); - 结论缓存起来,并显示在系统设置 → 当前状态 →「服务端读取通道」;
- 万一判断失效,内网尝试只等 2 秒就回退公网(不是默认的 10 秒)。
5. 迁移已有图片到云端
系统设置 → 图片存储 → 迁移:
- 先把本地
uploads/与db/备份一份(迁移会改写数据库里的图片引用)。 - 迁移是分批执行的(前端循环调用
storage_api.php?action=migrate并显示进度),中断了可以直接重跑——已迁移的记录会被跳过,不会重复计数。 - 迁移过程中会把文件重命名为内容指纹名,并同步改写数据库引用。
- 迁移完成后本地文件不会自动删除,因为读取是「本地优先」的:本地还在就继续走本地,删掉本地文件后才真正走云端。删本地文件前先全站看图确认。
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. 排障手册(可直接复制的命令)
# ① 判断是不是防盗链: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/,再动手。
安全注意事项
部署到公网前请务必处理以下事项:
- 修改默认口令:默认账号
admin / 123456必须在首次登录后立即更换。 - 「自动获取」会写入存储:
fetch_movie.php已要求登录后才能调用,且只请求固定的豆瓣域名、只把文件写入uploads/posters/;表单回传的fetched_poster_key与old_poster都要通过「目录前缀 + 文件名 + 数据库引用」三重校验才会被采用或删除。若你的部署不需要该功能,可直接删除fetch_movie.php并移除add.php里的按钮。action=thumb是服务端代取,必须保持白名单:它以服务端身份去取u指定的图片,若放开任意 URL 就是SSRF 跳板。代码里validateDoubanImageUrl()只放行https://img*.doubanio.com/view/photo/…,改动时务必保留。环境变量MOVIELOG_DOUBAN_IMG_ALLOW是给本地 mock 测试留的豁免口子(默认不设置即完全关闭),生产环境不要设置。- 下拉联想的候选文本来自豆瓣,前端一律转义后再插入:不要改成直接拼
innerHTML,否则豆瓣侧内容可注入脚本。
- 删除或保护
test_crop.php:该文件是开发调试页,没有登录校验,仅用于验证 Base64 图片能否落盘,生产环境应直接删除。 - 关闭调试输出:
config.php顶部当前为error_reporting(E_ALL)+display_errors=1,上线前请改为关闭或仅写日志,避免泄露路径与数据库信息。 - 保证 CA 证书可用:图片下载与「自动获取」都依赖
curl,且fetch_movie.php已启用证书校验(CURLOPT_SSL_VERIFYPEER = true)。若服务器缺少 CA 证书束(curl.cainfo为空,常见于最小化安装),请求会失败,请安装ca-certificates包。 - 保护数据目录:
db/(含movies.db)与uploads/应禁止直接目录列举;SQLite 文件更应通过 Web 服务器规则拒绝任何 HTTP 访问。Nginx 示例:location ~ ^/(db|uploads/.*\.(db|sqlite)) { deny all; } - 限制上传目录执行权限:确保
uploads/下不允许执行 PHP。
与对象存储相关的额外事项
- 桶不要长期开公共读:直连存储要求桶允许匿名读取,等于任何拿到 URL 的人都能看图。私人 / 家庭内容请改用「服务端代理」,把桶设为私有——那才是真正的访问控制。
- 防盗链不是访问控制:
Referer是客户端可伪造的普通请求头,curl -H Referer就能绕过。它只能挡住「别的网站盗链」,不能保护内容。别把它当成安全边界。 - 保护好
db/.settings.key:云厂商 AccessKey 以密文存在数据库里,这个文件是唯一的解密密钥。应随db/一起纳入访问控制与备份策略;优先改用实例角色(RAM / CAM 角色)免密钥方案,从根上不落长期密钥。 media.php是新增的对外接口:它按k参数从对象存储取对象。仓库里已做对象键白名单(防目录穿越)与类型白名单(防同源 XSS)两层校验,改动此文件时务必保留,不要把$_GET['k']直接拼进存储请求。- 不要把上游
Content-Type原样透传:桶里若有.html/.svg,透传类型会让它在本站域名下执行(同源 XSS)。始终按扩展名白名单决定,并加X-Content-Type-Options: nosniff。
已知限制与待改进
movies.photo_path与viewers.name是历史遗留字段,保留用于兼容旧数据与 CSV 导入,新代码统一使用movie_photos与person_id。- 除
delete.php通过 GET 触发(带前端confirm)外,其余写操作均为 POST;删除类接口未做 CSRF Token 校验。 - 暂无分页,数据量增大后首页与统计页需要补充分页或懒加载。
config.php的initDB()在每次请求都会跑一遍建表与迁移,数据量或并发上升后可改为一次性迁移脚本。- 无「回收站」,删除电影与其图片文件不可恢复。
assets/placeholder-poster.jpg仍是代码中的兜底引用,仓库未包含该文件;页面在检测到 placeholder 时会改用 CSS 占位块渲染,正常使用不受影响。assets/default-avatar.png已随仓库提供(默认头像),并在资源缺失时由getAvatarUrl()退化为内联 SVG 剪影。- 「直连存储」模式下裁剪功能不可用:裁剪需要读取图片像素,直连时图片来自对象存储域名,必须同时满足「桶配 CORS」与「
<img crossorigin="anonymous">」两个条件才能读像素;本仓库未加crossorigin属性,因此直连模式下只能上传、不能二次裁剪。需要裁剪请改用「服务端代理」(同源,两个条件全免)。详见《图片存储与对象存储》§3③。 - 服务端代理模式下所有图片流量经服务器中转,会占用服务器出网带宽,并给每张图增加一次服务器↔存储的往返(同地域约 10–30 ms,跨地域约 60–100 ms)。图片量大且可公开时可考虑改回直连。
- 内网 endpoint 只在服务器与桶同地域时生效。跨地域部署下这条免费通道用不了,程序会自动跳过并走公网(详见 §4)。
- 迁移到云端后本地文件不会自动删除,需要自行确认后再清理;本地优先策略意味着本地文件存在时始终读本地,云端副本仅作备份。
- 下拉联想会随输入向豆瓣实时发请求(已做 320 ms 防抖 + 会话内结果缓存 + 丢弃过期请求,同一关键词只查一次),但服务端本身没有做接口级限流。若担心被豆瓣限流,可把
add.php里的DEBOUNCE调大,或让下拉只在按下「📡 自动获取」时才工作。 - 缩略图代理(
action=thumb)每张图都要经服务器回源一次。已加Cache-Control: max-age=86400,同一张图在浏览器侧一天内只回源一次,正常使用量很小;若把下拉列表做得很大、或候选很多,建议改成在服务端落小图缓存。
版本控制说明
以下内容为运行时产物或敏感数据,不纳入版本控制(见 .gitignore):
db/(SQLite 数据库文件)uploads/(用户上传的图片)*.log、编辑器与系统临时文件
许可
个人项目,未指定开源许可。