v29 第 29 批(9 条):日程定位/复选框提示/名册错乱三处真因修复 + 读书落盘 + 导航四设置 + 笔记阅读态与共享
前 4 条是"上批改了但用户仍报"的,这次先拿真机证据再动手,三处真因如下:
1. 首页没定位到红线 —— 真根因:定位脚本的 PHP 条件是
`if (!$todayEvents || empty($timed))`,**今天只有全天日程时整段脚本不渲染**
(实测 hasScript=False、scrollTop=0),而 .ag-grid(0–24h) 与红线都在。
有分时段日程时定位其实完全正确(偏差 0px)。已改成 `if (!$todayEvents)`。
2. 任务复选框提示还在 —— 真根因:`_task_check.php` 里那句无障碍文字
`<span class="visually-hidden">` 用的类名**在 app.css 里从来没有定义过**
(项目一直只用 .sr-only)→ 没有规则 = 普通 span = 直接可见。
已把两个类名合并到同一条规则(clip 隐藏,仍可读屏)。
3. 名册完全错乱 —— 真根因:v29 重写平铺列表时**漏了一个 `</div>`**(闭合 .person),
浏览器把下一张卡嵌进上一张里,实测 .person-main 只剩 24~151px(本该 ~800px)。
`php -l` 与全部静态断言都抓不到,只有真机量 DOM 才现形。已补上闭合。
另把 v28 引入的多列 grid 改回单列(它与 content-visibility 冲突)+窄屏动作区折行。
4. 读书慢 —— 按用户给的思路「做一个同步,然后落盘」:
新增 weread_snapshots 表 + App\WeReadCache(按人+数据集存 JSON);
渲染路径改走 notebooksCached()/readStatsCached()(永不联网);
同步从"渲染之前"挪到"**响应之后**"(queueBackgroundSync + fastcgi_finish_request)。
5. 导航设置改成右上角**下拉菜单 + 四视图**(addr/general/board/engine),
表单带 back 域 → 保存后回原来那一项。
🔴 顺带修掉一个真 bug:`if (!$inSettings && $canWrite)` 的 endif 漂到了文件末尾,
把 board/engine 两个视图也包进了"非设置态" → 打开时标题对、**内容是空的**。
6. 笔记列表缩略图:cover → contain + 固定高度(按比例完整显示,不裁切)。
7. 笔记段落编辑器:加 Markdown 工具条 + 斜线命令(行首打 /co → 回车补全代码块),
两者共用同一份命令表。
8. 笔记点进来默认**阅读态**(图文混排、附件在最下方、图片四档宽度、点「编辑」才可改);
清单块并入 Markdown —— 但**历史 list 块转写成 `- 项` 文本,老数据不丢**。
9. 笔记共享改成**逐人 + 权限**(note_shares,与 resource_shares 同构);
可见性收敛成全站唯一的 Note::VIS_COND;迁移用一次性哨兵。
This commit is contained in:
+110
-1
@@ -108,6 +108,22 @@ final class Schema
|
||||
* ⚠️ `person_roster.group_id` **保留**(vCard CATEGORIES 与老代码的读点),
|
||||
* 启动时把已有分组**逐一迁成同名标签**(幂等:按 (owner,person,tag) 唯一键)。
|
||||
* 名册页从「按分栏展示」改成「平铺 + 顶部标签筛选」,拖动仍可排序。
|
||||
* 29 → **微信读书数据落盘(用户 2026-10-08 第 4 条)**:新增 `weread_snapshots`
|
||||
* (按 (人, 数据集) 存 JSON 快照)。动机是用户的原话:
|
||||
* 「读书还是慢。是不是可以做一个同步,然后落盘,这样就不用每次都读取
|
||||
* 微信读书的数据了。」
|
||||
* ⇒ `notebooks()` / `readStats()` 在**页面渲染路径**上不再打网关,
|
||||
* 只读快照;打接口只发生在同步时(手动 / 6 小时到期)。
|
||||
* ⚠️ 快照有 `updated_at`,过期(默认 24 小时)仍会触发一次后台同步,
|
||||
* 但**渲染永远不等它** —— 拿旧快照先渲染,同步结果下次访问可见。
|
||||
* 30 → **笔记共享改成"逐人 + 权限"(用户 2026-10-08 第 9 条)**:新增
|
||||
* `note_shares`(与 `resource_shares` 同构)。动机是用户的原话:
|
||||
* 「笔记的共享,和其他的模块一样,选择共享人,选择是只读还是可以读写。和资源类似。」
|
||||
* ⇒ 取消 v27 的"一个 `is_shared` 布尔(共享给全家)",
|
||||
* 老数据升级时**展开成逐人记录**(给家里每个人一条 read)。
|
||||
* ⚠️ 用一次性哨兵(`note_shares_migrated_v29`),理由同 v28 那次:
|
||||
* 迁移是"一次性数据转换",不是"持续同步",
|
||||
* 否则用户取消共享后下次启动又被灌回来(本项目已栽过 4 次)。
|
||||
*/
|
||||
/**
|
||||
* 结构版本(**人读**用:日志、设置页上显示"库结构是哪一版")。
|
||||
@@ -115,7 +131,7 @@ final class Schema
|
||||
* ⚠️ 它**不再**作为"要不要跑升级"的判据 —— 见 fingerprint() 的说明。
|
||||
* 它仍然要手工维护,因为它给人看(排查时一眼知道代码到哪一版了)。
|
||||
*/
|
||||
public const VERSION = '28';
|
||||
public const VERSION = '30';
|
||||
|
||||
/**
|
||||
* 「源码指纹」:拿**本文件自身的内容**算一个哈希,作为"要不要跑升级"的判据。
|
||||
@@ -1008,6 +1024,62 @@ CREATE TABLE IF NOT EXISTS `person_tags` (
|
||||
CONSTRAINT `fk_pt_person` FOREIGN KEY (`person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='名册标签(v28:一个人可挂多个标签,取代"一人一组")'
|
||||
SQL,
|
||||
<<<'SQL'
|
||||
/* 🔴 v29 用户第 4 条:**微信读书的数据落盘,页面只读本地**。
|
||||
用户原话:「读书还是慢。是不是可以做一个同步,然后落盘,这样就不用每次都
|
||||
读取微信读书的数据了。」
|
||||
⇒ 症结确实在这:`notebooks()` 与 `readStats()` 每次渲染都**实时打网关**,
|
||||
一次读书页要打好几次外部请求(且官方接口本身就慢)。
|
||||
实测口径:以前每个请求都要等外部接口返回,`SYNC_BUDGET` 只能限制"同步"那一段,
|
||||
限不住"每次渲染都去读统计"那一段。
|
||||
⇒ 做法:**一份按 (人, 数据集) 存的 JSON 快照**。
|
||||
· 页面渲染只读这张表(本地查询,毫秒级);
|
||||
· 真正打接口只发生在**同步**时(手动「立即同步」或 6 小时到期的自动同步);
|
||||
· 同步成功后把相关快照一起刷新。
|
||||
⚠️ 为什么用"一个 JSON 列"而不是把统计拆成多张表:
|
||||
这些数据是**只读的远端结果**、字段完全由官方决定(说改就改),
|
||||
本站从不按其中任何字段做查询。拆表=把别人的字段设计背到自己身上,
|
||||
官方一变就要迁移。存 JSON = 官方怎么变都能原样兜住。
|
||||
⚠️ 为什么不像封面那样塞进 `book`/`attachment`:它不是"我们的实体",
|
||||
是一次外部调用的缓存,有明确的**时效性**(`updated_at`),
|
||||
过期就该重新拉 —— 与"实体数据"的生命周期完全不同,混在一起会后患无穷。 */
|
||||
CREATE TABLE IF NOT EXISTS `weread_snapshots` (
|
||||
`person_id` INT UNSIGNED NOT NULL,
|
||||
`k` VARCHAR(40) NOT NULL COMMENT '数据集名:notebooks / stats_overall / stats_weekly …',
|
||||
`v` LONGTEXT DEFAULT NULL COMMENT '原样存下来的 JSON(官方字段,本站不解析、不索引)',
|
||||
`updated_at` DATETIME NOT NULL COMMENT '这次快照是什么时候拉的(判过期用)',
|
||||
PRIMARY KEY (`person_id`, `k`),
|
||||
KEY `idx_ws_updated` (`updated_at`),
|
||||
CONSTRAINT `fk_ws_person` FOREIGN KEY (`person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='微信读书的远端数据快照(v29:页面只读本地,不再每次打网关)'
|
||||
SQL,
|
||||
<<<'SQL'
|
||||
/* 🔴 v29 用户第 9 条:「笔记的共享,和其他的模块一样,选择共享人,
|
||||
选择是只读还是可以读写。和资源类似。」
|
||||
⇒ 把 v27 那个"一个 is_shared 布尔(共享给全家)"升级成**逐人共享 + 权限**。
|
||||
⚠️ 为什么不是"保留 is_shared,再加一张人表":
|
||||
那样会出现两种共享语义并存("全家可见" + "给这几个人"),
|
||||
判定优先级、列表怎么算、谁能看到 —— 全是边界问题。
|
||||
统一成一种(逐人 + 权限)之后,判定只有一处:
|
||||
`owner 本人 or 在某条 share 记录里`。
|
||||
⚠️ `notes.is_shared` 列**保留但不再作为判据**(升级时把老的"1"展开成
|
||||
"给全家每个人一条 read 记录",迁移完就没它的事了)。
|
||||
保留列是为了老库升级时能读到它(见 migrateSharedNotes)。
|
||||
⚠️ 与资源共享表**同构**(列名/枚举都一样):两处的权限语义要一致,
|
||||
表结构一样,读代码的人一眼就知道"跟资源那套是一回事"。 */
|
||||
CREATE TABLE IF NOT EXISTS `note_shares` (
|
||||
`note_id` INT UNSIGNED NOT NULL,
|
||||
`person_id` INT UNSIGNED NOT NULL COMMENT '共享给谁(**不含**作者 —— 自己写的本来就能改)',
|
||||
`perm` ENUM('read','write') NOT NULL DEFAULT 'read' COMMENT 'read=只读 / write=可读写',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`note_id`, `person_id`),
|
||||
KEY `idx_ns_person` (`person_id`),
|
||||
CONSTRAINT `fk_ns_note` FOREIGN KEY (`note_id`) REFERENCES `notes` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_ns_person` FOREIGN KEY (`person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='笔记共享给别人:可只读、可读写(v29:取代"共享给全家"那个布尔)'
|
||||
SQL,
|
||||
];
|
||||
}
|
||||
@@ -1894,6 +1966,43 @@ SQL);
|
||||
}
|
||||
});
|
||||
|
||||
/* 🔴 v29 用户第 9 条:「笔记的共享,和其他的模块一样,选择共享人,
|
||||
选择是只读还是可以读写。和资源类似。」
|
||||
⇒ 把 v27 的"`is_shared = 1`(共享给全家)"展开成**逐人记录**。
|
||||
怎么做:对每一条 `is_shared = 1` 的笔记,给**家里其他每个人**写一条
|
||||
`perm = 'read'`(读得到,但不能改 —— 原来的语义就是"全家可见",
|
||||
并没有给过他们改的权限,迁移必须**保持原语义**,不能顺手扩大权限)。
|
||||
⚠️ 排除作者自己:作者本来就全能,给自己写一条共享记录会污染"共享给谁"的列表。
|
||||
⚠️ 一次性哨兵(`note_shares_migrated_v29`)—— 理由与 v28 那次完全一样:
|
||||
迁移是"一次性数据转换",不是"持续同步"。否则用户把某人从共享里去掉后,
|
||||
下次启动又被灌回来(本项目已栽过 4 次,见 v28 的详细记录)。
|
||||
⚠️ 这个迁移**必须在 note_shares 建好之后**跑(外键指向 notes/persons,
|
||||
两张表都在 statements() 里先建出来了)。 */
|
||||
self::tryStep('笔记共享展开成逐人记录(全家可见 → 逐人只读)', static function () use ($db) {
|
||||
$flag = 'note_shares_migrated_v29';
|
||||
$has = $db->query("SELECT COUNT(*) FROM `settings` WHERE `k` = '" . $flag . "'")->fetchColumn();
|
||||
if ((int)$has > 0) {
|
||||
return;
|
||||
}
|
||||
if (self::hasTable($db, 'note_shares')) {
|
||||
// 只处理"曾经标过共享"的笔记;逐人展开,权限一律 read(保持原语义)
|
||||
$db->exec(
|
||||
"INSERT IGNORE INTO `note_shares` (note_id, person_id, perm)
|
||||
SELECT n.id, p.id, 'read'
|
||||
FROM `notes` n
|
||||
JOIN `persons` p ON p.family_id = n.family_id
|
||||
WHERE n.is_shared = 1
|
||||
AND p.id <> n.person_id
|
||||
AND p.kind = 'member'"
|
||||
);
|
||||
}
|
||||
$fid = (int)$db->query('SELECT MIN(id) FROM families')->fetchColumn();
|
||||
if ($fid > 0) {
|
||||
$db->prepare("INSERT IGNORE INTO `settings` (family_id, `k`, `v`) VALUES (?,?,?)")
|
||||
->execute([$fid, $flag, date('Y-m-d H:i:s')]);
|
||||
}
|
||||
});
|
||||
|
||||
// 被容错跳过的数据步骤记进设置,系统设置页会把它显出来 ——
|
||||
// 不然「升级没跑完」这件事只活在日志里,跟没发生一样。
|
||||
if (self::$stepErrors) {
|
||||
|
||||
Reference in New Issue
Block a user