fix(notes): 调过宽度的图片保存后变孤儿(#18)

用户报「笔记功能,上传图片失败」。实际不是上传失败,是上传成功后
保存那一步把图片弄丢了。

根因:图片块 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
This commit is contained in:
2026-10-28 16:03:36 +08:00
parent ff818a3deb
commit fc81347e8d
+40 -3
View File
@@ -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();