From fc81347e8d25515adff1a71745e2fd91780c9027 Mon Sep 17 00:00:00 2001 From: laoyang Date: Wed, 28 Oct 2026 16:03:36 +0800 Subject: [PATCH] =?UTF-8?q?fix(notes):=20=E8=B0=83=E8=BF=87=E5=AE=BD?= =?UTF-8?q?=E5=BA=A6=E7=9A=84=E5=9B=BE=E7=89=87=E4=BF=9D=E5=AD=98=E5=90=8E?= =?UTF-8?q?=E5=8F=98=E5=AD=A4=E5=84=BF=EF=BC=88#18=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户报「笔记功能,上传图片失败」。实际不是上传失败,是上传成功后 保存那一步把图片弄丢了。 根因:图片块 content 有两种形态 - `photos/xxx.jpg` —— 宽度 100% 时,前端不写后缀 - `photos/xxx.jpg|50` —— 宽度调到 20~99% 时,serialize() 加上宽度 但 Note::claimUploads() 与 Note::files() 直接拿 content 去匹配 attachments.key(`WHERE key IN (...)` / `isset($used[$key])`), 带后缀的那个**永远匹配不到**,于是: 1. 草稿附件(target_id=0)认领失败 ⇒ 图片变孤儿,笔记里图没了 2. files() 排除不掉已用作图片块的附件 ⇒ 同一张图既当图片块、 又在底部附件区重复出现 默认 100% 不写后缀,所以表现为"有时好有时坏", 极易被误判成上传环节的问题。 修法:新增 Note::imageKey() 统一剥 `|宽度` 后缀, claimUploads() 与 files() 都先过它。 刻意修在**服务端**、而不是"前端别写后缀":后者会把宽度功能废掉 —— 渲染时要按 content 里的宽度出图(见 templates/notes/form.php 的 $imgParts 与 templates/notes/index.php:98,那两处本来就剥了, 所以只有服务端这两处漏了,属于不对称实现)。 updateBlock() 走同一条 claimUploads(),自动被覆盖。 验证(本地 PHP 8.5 + MariaDB,真实 UI 操作,非手搓 DOM): - 主判据 _tooling/_b35_width_falsify.py:修复前 100%→1712 成功 / 50%→target_id=0;修复后两条路径都认领成功,7 通过 0 失败, 且 content 仍带 `|50`(宽度功能没被牺牲) - 附件不重复 _tooling/_b35_dup_attach.py:阅读态底部附件 0 条、 图片块 1 张、img 61px vs figure 990px(宽度真的生效),5 通过 0 失败 - 判据有牙齿:把 claimUploads() 注入回旧形态后立刻 2 条 FAIL, 恢复后回到 7 通过 0 失败 工单 #18 --- app/Note.php | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/app/Note.php b/app/Note.php index 2bc0aef..0f623d0 100644 --- a/app/Note.php +++ b/app/Note.php @@ -298,7 +298,12 @@ final class Note $used = []; foreach (self::blocks($noteId) as $b) { if ((string)$b['kind'] === 'image') { - $used[(string)$b['content']] = true; + // 🔴 工单 #18:content 可能带 `|宽度`,必须剥掉再比; + // 否则"调过宽度的图"排除不掉,会既当图片块又在附件区重复出现。 + $k = self::imageKey((string)$b['content']); + if ($k !== '') { + $used[$k] = true; + } } } $out = []; @@ -452,6 +457,34 @@ final class Note return true; } + /** + * 从图片块的 content 里取出**真正的附件 key**(剥掉 `|宽度` 后缀)。 + * + * 🔴 工单 #18(用户 2026-10-28「笔记功能,上传图片失败」):图片块 content 有两种形态 + * —— `photos/xxx.jpg` 与 `photos/xxx.jpg|50`(前端 `serialize()` 在宽度不是 100 时写后缀)。 + * **凡是拿 content 当 key 用的地方都必须先过这里**,否则带后缀的那个永远匹配不到。 + * + * ⚠️ 曾经的 bug:`claimUploads()` 与 `files()` 直接拿 content 匹配 `attachments.key`, + * 于是"把图片调成 50% 再保存" ⇒ 草稿附件(target_id=0)认领不到 ⇒ 图片变孤儿, + * 用户看到的就是**图片上传失败 / 保存后图没了**。 + * 默认 100% 不写后缀,所以表现为"有时好有时坏",极易被误判成上传问题。 + * + * ⚠️ 修在**服务端**而不是"前端别写后缀":后者会把宽度功能废掉 + * (渲染时要按 content 里的宽度出图,见 templates/notes/form.php 的 $imgParts)。 + * + * @param string $content 图片块 content,形如 `abc` 或 `abc|50` + * @return string 纯 key(可能为空串) + */ + public static function imageKey(string $content): string + { + $c = trim($content); + if ($c === '') { + return ''; + } + $p = explode('|', $c, 2); + return trim($p[0]); + } + /** * 把本次上传的图片块附件从"草稿态"认领到这条笔记上。 * @@ -469,8 +502,12 @@ final class Note { $keys = []; foreach ($blocks as $b) { - if ($b['kind'] === 'image' && $b['content'] !== '') { - $keys[] = $b['content']; + if ($b['kind'] === 'image') { + // 🔴 工单 #18:必须先剥 `|宽度` 后缀,否则带后缀的 key 永远匹配不到 + $k = self::imageKey((string)$b['content']); + if ($k !== '') { + $keys[] = $k; + } } } $db = Database::get();