Commit Graph
12 Commits
Author SHA1 Message Date
laoyang 558c0db576 评价感受「感动」的图标换成 💖,修缺字形
🥹(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        同步感受清单里的图标
2026-09-22 15:07:10 +08:00
laoyang dc5e788f27 评价本身也能填「观后感受」
- lib/db.php:reviews 表新增 feelings 列。建表语句里带上,同时补一条
  ALTER TABLE 迁移 —— 上一版已经上线过没有该列的 reviews 表,老库要能自动补列。
- lib/reviews.php:saveReview() 接收感受并落库;getMovieReviews()/getMyReview()
  把 feelings 解析成 key 列表(旧记录为 NULL → 空数组,不报错)。
- detail.php:评价表单加了「我看完的感受(可多选)」,与讨论用的是同一套感受;
  修改评价时原勾选自动回填,取消勾选即清空;已保存的感受在评价正文下方以只读标签展示。

感受仍然只存 key(happy/sad/…),emoji 与文案统一在 feelingCatalog() 里维护。
2026-09-22 13:03:04 +08:00
laoyang a2c26ad002 新增评价与讨论:打分 1-10 + 观后感受 emoji
- 规则:只要有登录账号就能评价,与「是不是观影人」无关;评价归属 admins
- 数据层 lib/reviews.php:
  · reviews(movie_id + admin_id 唯一索引,一个账号一部电影只一条)
  · review_comments(feelings 存感受 key 的 JSON 数组)
  · 感受目录 feelingCatalog():12 种,每种一个专属 emoji,文案与 emoji 不入库
- detail.php 新增「💬 评价与讨论」:1-10 打分(原生 radio,无 JS 也能用)、
  评价正文、评价列表、每条评价下的讨论与多选感受、均分概况
- index.php:有评价的电影卡片显示 ⭐ 均分
- delete.php:删电影时显式清理评价与讨论(SQLite 外键级联默认未开启)
- 顺带修一个隐患:delete.php 取一行后没关游标,读锁一直挂着,
  后续同一连接上的 DELETE 拿不到写锁,页面会卡满 60 秒
2026-09-22 11:42:27 +08:00
laoyang 7d199d5b3a feat: 详情页图片改用本页灯箱,不再 window.open 弹新窗口
以前点图片会 window.open 弹一个新窗口,没有关闭按钮、来回切换很别扭。
现在把「海报 → 票根 → 观影照片」收进一个序列,点任一张在当前页弹出查看。

交互
- 左右箭头循环翻看,底部显示「3 / 8」进度与图片说明,✕ 关闭。
- 键盘 ← / → 翻页、Esc 关闭;点画面空白处关闭(点图片或按钮不关)。
- 移动端左右滑动切换;手机上箭头缩小并内收,给图片让出更多宽度。
- 弹出时给 <body> 加锁滚动;预取相邻两张,翻页不白屏。
- 只有一张图时不显示左右箭头;关闭后焦点回到原来点开的那张图。
- 英雄区海报也变成可点开(占序列首位);无真实海报时维持占位块不变。

实现
- 图片序列在页面顶部一次性组装,英雄区与图库共用同一套 data-lb-index 下标,
  不再两处各算一遍;渲染与判断都统一走 storageExists / storageUrl。
- GALLERY 通过 json_encode 输出,带 JSON_HEX_TAG / HEX_AMP / HEX_APOS / HEX_QUOT,
  避免豆瓣侧或文件名里的 </script> 截断脚本。
- 候选文本一律经 textContent / caption 写入,不拼 HTML。

两个实现坑(本批修掉)
- 作者样式 .lb-btn{display:flex} 优先级高于 UA 的 [hidden]{display:none},
  单图时箭头实际隐藏不掉 —— 必须显式写 .lb-btn[hidden]{display:none}。
- 灯箱复用同一个 <img>,而 main.js 的破图兜底会给 <img> 打上
  data-fallback-applied 后就不再处理;每次换图要 removeAttribute 重新武装,
  否则只要有一张失败过,后续图片就不再有占位兜底。

验证
- php -l 通过;内联 JS/CSS 用「模拟 PHP 渲染」方式校验通过。
- 新增 lightbox_test.js(jsdom,对真实渲染出的详情页跑):38 项全通过,
  覆盖打开/翻页/循环/键盘/Esc/点空白关闭/点图片不关/单图隐藏箭头/破图标记复位。
  这两个坑正是它抓出来的(delete dataset.x 在 jsdom 下不生效,故改用 removeAttribute)。
- fetch_movie 端到端 76 项、storage 回归 196 项全通过。
- A/B 47 个响应:只有详情页(及会重定向到详情页的提交响应)6 个有差异,其余 41 个不变。
- 线上双 PHP(7.4/8.1)用真实数据渲染通过:4 张图序列、URL 为本站代理地址、
  结构齐备、无 window.open。
  (注:以 movie 身份跑 CLI 时 db/.settings.key 不可读,云端配置会被判为未完成、
  所有图片都判成「不存在」;冒烟脚本改为向进程内缓存注入一份假配置绕过,
  不写库、不碰真实密钥。这只影响探针,不影响以 www 运行的站点。)
2026-09-22 08:59:12 +08:00
laoyang 618409854c feat: 导航栏左上角的系统名做成「回到首页」链接
原来要回首页得先点开右侧折叠菜单再点「首页」(两步操作,在手机上尤其别扭),
现在点左上角系统名一下就能到。

nav.php
- <div class="nav-brand"> 改为 <a href="index.php" class="nav-brand">,
  并补上 title 与 aria-label(无障碍:屏幕阅读器能读出这是个「回到首页」链接)。
- 因为是原生 <a>,Ctrl/中键点击可在新标签页打开,不再需要 JS。

css/style.css
- 给 .nav-brand 加点击热区:padding + 等量负 margin 抵消,
  保证文字的视觉位置与改动前完全一致(600/768px 两个断点下 navbar 的
  padding 变窄时,两者依然自然抵消)。
- 加 hover 底色反馈,让「这是可点的」有视觉提示。
- 加 :focus-visible 轮廓:键盘 Tab 聚焦时可见,鼠标点击时不出现,
  避免多出一层多余描边。

验证
- nav.php 语法通过(双 PHP 版本 7.4 / 8.1),内联 JS 用「模拟 PHP 渲染」方式
  做 node --check 通过。
- CSS 大括号配平(83/83)。
- fetch_movie_test.py 端到端 79 项、storage_test.py 回归 196 项全通过。
- A/B 渲染比对 47 个响应:45 个含导航的页面有差异,但**差异只有那一行**
  <div> → <a>(含注释与闭合标签),其余内容逐字节一致;不含导航的
  登录失败页与退出页完全未变。
- 线上双 PHP 版本真实渲染导航栏:链接形态、title/aria-label、折叠菜单均在,
  无 PHP 报错泄漏;公网 style.css 与本地 sha256 一致。
2026-09-22 08:01:00 +08:00
laoyang 99c22d17bd feat: 片名输入加下拉联想,不必把片名打全就能选片
输入片名时实时下拉候选(海报缩略图 + 片名 + 原名 + 年份 + 电影/剧集标注),
选中即自动填充影片信息,省去「打全片名再点按钮」这一步。

fetch_movie.php
- 新增 action=suggest:只回候选列表,不取详情、不下载海报、不写任何文件,
  可随输入安全地频繁调用;会过滤「影人」类条目、按豆瓣条目 id 去重、默认限 8 条。
- 新增 action=thumb:服务端代取豆瓣缩略图。豆瓣图片有防盗链(只有 Referer 为
  豆瓣域名才返回 200,其它一律 403),浏览器直连必然被拒,只能由服务端带
  Referer 代取;并加 Cache-Control 让浏览器缓存,重复展开不重复回源。
- 新增 ?id=豆瓣条目id:跳过搜索直接取详情。下拉选中后走这条路,既省一次搜索,
  也避免「再按片名搜一次、结果选错条目」。
- 安全:新增 validateDoubanImageUrl() 严格白名单(只允许
  https://img*.doubanio.com/view/photo/…),否则 thumb 就成了 SSRF 跳板;
  接口地址的测试用覆盖项一并写进注释。

add.php
- 片名输入框改为「输入即联想」:320ms 防抖、会话内结果缓存、用 AbortController
  丢弃过期在途请求、↑↓ 选择 / Enter 确认 / Esc 关闭、点击外部关闭、
  中文输入法组字期间不发请求也不劫持 Enter(此时 Enter 是选字)。
- 候选里的豆瓣文本一律转义后再插入,避免把对方内容当 HTML 执行。
- 「📡 自动获取」按钮保留:候选里没有想要的条目时仍可按回车或点按钮,由服务端
  挑最匹配的条目;按钮与下拉选中复用同一套获取逻辑(runFetch)。

README.md
- 接口文档改为四种模式(q / id / suggest / thumb),补上参数、响应样例、
  防盗链原因与白名单说明;使用说明与安全注意事项同步更新。

验证
- fetch_movie_test.py 端到端 79 项全通过(新增 suggest / thumb / id 三组用例,
  含「suggest 与 thumb 不写任何文件」「非白名单地址一律 400」)。
- storage_test.py 回归 196 项全通过。
- A/B 渲染比对 47 个响应:仅 add.php 的两个页面有差异,且差异全部落在下拉联想
  相关代码上,其余响应逐字节一致。
- 已部署到线上,双 PHP 版本(7.4 / 8.1)真实抓取通过:suggest 返回候选、
  thumb 代取回 27KB JPEG、非白名单地址被拒、id= 直连取详情成功。
2026-09-22 07:36:05 +08:00
laoyang ddf2eeafaa feat: 「自动获取」改用豆瓣数据源,海报自动下载入库
此前的 OMDb 数据源必须用英文片名,国产片覆盖极差。改用豆瓣公开接口后
中文片名直接可搜,冷门片/老片/剧集也能命中。

fetch_movie.php(全量重写)
- 搜索走 movie.douban.com/j/subject_suggest;详情走移动端
  m.douban.com/rexxar/api/v2/movie/<id>(PC 详情页有反爬 302,故用移动端接口)
- 海报下载后写入 uploads/posters/,文件名沿用内容指纹规则
- 新增登录鉴权:未登录返回 401(此前该接口完全公开)
- 启用 TLS 证书校验(原为 CURLOPT_SSL_VERIFYPEER=false)
- 魔数兜底 + 12MB 上限:挡掉反爬返回的 HTML 页面与异常大响应
- old_poster 残留清理:重复点击时删掉上一张,且仅删「未被数据库引用」的文件
- 接口地址支持环境变量覆盖,便于用 mock 做端到端测试

lib/helpers.php
- 新增 isValidStoredKey():校验表单回传的存储键,防目录穿越与越权引用

add.php
- 前端改用新字段(豆瓣评分 / 地区 / 又名),并把 poster_key 放进隐藏域回传
- 提交时「手动上传的海报优先」,没有才采用自动获取已入库的海报

README.md
- 同步为豆瓣数据源说明,移除已过时的 OMDb API Key 与 SSL 校验两条安全提示

验证
- 本地 mock 豆瓣端到端:33/33 通过
- 既有存储回归套件:196/196 通过
- A/B 归一化比对:除 add.php 按预期变化外,其余 45 个响应完全一致
- 线上实测:流浪地球 / 哪吒之魔童闹海 均抓取成功,海报落盘为合法 JPEG
2026-09-21 23:05:40 +08:00
laoyang 294c34bbc2 feat: 补齐默认头像资源,修复「删除头像」静默失效
问题:添加观影人页面的头像预览指向 /assets/default-avatar.png,但仓库里
从来没有这个文件 → 该预览一直是坏图;同时未上传头像时会按姓名首字母生成
彩色 SVG 头像,导致「没有头像」这一种状态有两种表现,行为不统一。

改动
- assets/default-avatar.png(新增):200x200 中性灰人形剪影,随仓库分发;
  生成脚本见 _tooling/make_default_avatar.py(Pillow,4 倍超采样)。
- lib/helpers.php:新增 defaultAvatarUrl();getAvatarUrl() 在未上传头像时
  一律返回默认头像(不再生成首字母头像);资源缺失时退化为内联 SVG 剪影,
  彻底消除「兜底图本身就是坏图」这类问题。
- persons.php:
  * 后端开始处理 delete_avatar —— 此前前端设了标记、后端从未读取,
    点「删除」再保存头像原样保留,是个静默失效的按钮。
  * 上传新头像优先于删除标记,避免「先点删除、后又重新选图」时上传被忽略。
  * removeAvatar() 复用同一个隐藏域,重复点击不再叠加多个同名 input。
  * 表单预览统一走 getAvatarUrl()(顺带修掉头像键存在但文件丢失时仍渲染坏图)。
- README.md:更新头像兜底说明、目录树与「已知限制」。

验证
- storage_test.py 新增第 [21] 节 21 项断言(资源合法性、HTTP 可访问、四个分支、
  资源缺失降级、页面渲染、删除头像端到端),合计 196 项全绿。
- 47 个端点的 A/B 回归:剔除「预期的头像地址替换」与「persons.php 内联 JS 重写」
  后无任何其它差异(0 处未解释变更)。
- 第 10 批部署已上线:线上 assets/default-avatar.png 的 sha256 与本地逐字节一致,
  全站无 PHP 诊断泄漏,数据行数不变(movies 219 / persons 17 / viewers 449)。
2026-09-21 22:21:01 +08:00
laoyang 673a8abf78 feat: 云端图片存储(OSS/COS/七牛)+ 服务端代理读取,并补全文档
新增存储抽象层:图片可存本地或云对象存储,切换存储方式不影响已有图片。

存储层
- 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 条踩坑记录、可直接复制的排障命令手册
2026-09-21 22:06:02 +08:00
laoyang dd957d45bd refactor: 拆分 config.php 为 lib/ 模块,消除页面层重复代码
纯结构性重构,不改变任何功能。

- config.php 由 739 行缩减为 42 行,改为纯引导入口;新增 ROOT_DIR 常量
  统一拼接绝对路径(lib/ 内模块的 __DIR__ 会指向 lib/,必须替换)
- 新增 lib/ 五个模块 db.php / helpers.php / auth.php / movies.php / stats.php,
  由 config.php 按 db → helpers → auth → movies → stats 顺序加载
- add.php、edit.php 各自约 80 行重复的 POST 处理改为调用
  resolveCinemaId / resolvePersonIds / linkViewers / uploadImageFile /
  saveBase64Image / uploadMultiplePhotos / deleteStoredFile
- cinemas.php、persons.php 中重复 4 次的颜色校验改用 normalizeHexColor()
- toggleNewViewer / removeViewerRow / toggleNewCinema 下沉至 js/main.js,
  add.php 中的 removeViewer 统一更名为 removeViewerRow
- 删除死代码:imageUrl()(全项目无调用);cinemas.php 配色数组中本就会被
  array_slice 丢弃的 extra 颜色逻辑,简化为单条扁平 50 元素数组
- README 目录结构补充 lib/ 说明

验证方式:以便携 PHP 8.4 搭建 A/B 回归环境,播种固定数据后遍历 47 个
GET/POST 端点录制「状态码 + 响应体」。重构前后 42 个逐字节相同、2 个仅
上传文件名时间戳不同、3 个差异均为内联脚本下沉(已单独校验函数体逐字符
等价,剥离脚本后 HTML 标记完全一致)。24 个 PHP 文件语法检查全部通过。
2026-09-21 12:56:30 +08:00
laoyang 40e1bc5e56 feat: 完善电影记录系统源码与项目文档
功能模块:
- 观影记录:列表(瀑布流)/ 添加 / 编辑 / 详情 / 删除,删除时同步清理图片文件
- 首页快捷搜索 + 高级搜索(片名、影院、观影人、日期区间组合筛选)
- 影院管理:三段式信息 + 50 种预设配色,删除已引用影院时二次确认
- 观影人管理:头像裁剪上传 + 专属配色,已引用者禁止删除
- 图片处理:Cropper.js 裁剪票根/海报/观影照片/头像,多图上传,首字母 SVG 头像兜底
- OMDb 自动获取影片信息(fetch_movie.php 代理,返回 JSON)
- CSV 批量导入:UTF-8/GBK 自动识别,影院去重,事务保护,逐行错误报告
- 统计仪表板:总览 + 按影院/观影人/年份/近 12 个月排行
- 年度报告:按账号关联的观影人视角生成,含 Chart.js 月度与星期分布图
- 多用户:账号增删改、账号与观影人绑定、修改自己的用户名与密码
- 登录鉴权:Session + password_hash,全站页面登录校验

文档:
- 重写 README:功能特性、技术栈、目录结构、数据库结构、部署与使用指南、接口说明、CSV 格式、安全注意事项、已知限制
- 新增 .gitignore,排除 db/ 与 uploads/ 等运行时数据
2026-09-21 08:20:56 +08:00
laoyang 9ad5f3e0f5 Initial commit 2026-09-21 00:06:43 +00:00