Files
laoyang 37ba86321b 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;迁移用一次性哨兵。
2026-10-08 10:54:43 +08:00

138 lines
5.5 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 微信读书远端数据的**本地快照**(v29 用户 2026-10-08 第 4 条)。
*
* 用户原话:
* 「读书还是慢。是不是可以做一个同步,然后落盘,这样就不用每次都读取
* 微信读书的数据了。」
*
* ────────────────────────────────────────────────────────────────
* 为什么单开一个类、而不是塞进 `WeRead`:
* `WeRead` 的头部写得很清楚 ——「这个类**只负责取数据**,不碰数据库」。
* 那条约定是有价值的(网络与存储分开,测试可以只测取数不连外网)。
* 落盘是**另一件事**,所以放另一个类;`WeRead` 保持"干净的网络层"。
*
* 为什么是"JSON 快照"而不是"拆成表":
* 这些数据是**只读的远端结果**,字段完全由官方决定(说改就改),
* 本站从不按其中任何字段做查询、排序、JOIN。
* 拆表 = 把别人的字段设计背到自己身上,官方一变就要写迁移;
* 存 JSON = 官方怎么变都能原样兜住(渲染端按需取,取不到就当空)。
*
* 为什么渲染路径**永远不等**刷新(关键设计):
* `get()` 只读本地,永不联网。若快照过期,调用方可以"顺手"触发一次刷新,
* 但**必须先拿旧快照把页面渲染出来**,刷新结果留到下次访问可见。
* 反面做法是"过期就同步完再渲染" —— 那就又回到"点进来要等很久",
* 也就是用户这次报的那个问题。**"数据新鲜"永远排在"页面能立刻打开"之后。**
*/
declare(strict_types=1);
namespace App;
final class WeReadCache
{
/** 快照的数据集名 —— 只在这里登记,别处一律用常量,避免拼错字符串 */
public const K_NOTEBOOKS = 'notebooks';
/** 阅读统计(按 mode 各存一份:overall / weekly / monthly / annually) */
public static function statsKey(string $mode): string
{
return 'stats_' . $mode;
}
/**
* 读一份快照。
*
* @param bool $freshOnly true = 过期就当没有(返回 null)——
* 给那些"宁可不显示也不要旧数据"的场景用
* @return array{ok:bool,data:mixed,updated_at:int,stale:bool}
* ok=false 表示**本地没有**(从没同步过)
*/
public static function get(int $personId, string $key, bool $freshOnly = false): array
{
$miss = ['ok' => false, 'data' => null, 'updated_at' => 0, 'stale' => true];
if ($personId <= 0 || $key === '') {
return $miss;
}
$db = Database::get();
try {
$st = $db->prepare(
'SELECT v, UNIX_TIMESTAMP(updated_at) AS ts
FROM weread_snapshots WHERE person_id = ? AND k = ?'
);
$st->execute([$personId, $key]);
$row = $st->fetch();
} catch (\Throwable $e) {
// 表还没建(升级中)→ 当作"没有快照",绝不让缓存层把页面打挂
return $miss;
}
if (!$row) {
return $miss;
}
$ts = (int)($row['ts'] ?? 0);
$stale = (time() - $ts) > WeRead::SNAP_TTL;
if ($freshOnly && $stale) {
return $miss;
}
$data = json_decode((string)($row['v'] ?? ''), true);
return ['ok' => true, 'data' => $data, 'updated_at' => $ts, 'stale' => $stale];
}
/** 写一份快照(覆盖旧的)。失败静默 —— 缓存写不进去不该影响功能。 */
public static function put(int $personId, string $key, $data): void
{
if ($personId <= 0 || $key === '') {
return;
}
$json = json_encode($data, JSON_UNESCAPED_UNICODE);
if ($json === false) {
return;
}
try {
$db = Database::get();
$st = $db->prepare(
'INSERT INTO weread_snapshots (person_id, k, v, updated_at)
VALUES (?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE v = VALUES(v), updated_at = VALUES(updated_at)'
);
$st->execute([$personId, $key, $json]);
} catch (\Throwable $e) {
// 静默(缓存是加速手段,不是功能本体)
}
}
/** 最后一次落盘是什么时候(页面上显示"数据更新于 …") */
public static function updatedAt(int $personId, string $key): int
{
return (int)self::get($personId, $key)['updated_at'];
}
/** 这个人有没有**任何**快照(判断"是不是第一次用") */
public static function hasAny(int $personId): bool
{
if ($personId <= 0) {
return false;
}
try {
$st = Database::get()->prepare(
'SELECT COUNT(*) FROM weread_snapshots WHERE person_id = ?'
);
$st->execute([$personId]);
return (int)$st->fetchColumn() > 0;
} catch (\Throwable $e) {
return false;
}
}
/** 清掉一个人的全部快照(换 Key / 停用自动同步时用) */
public static function forget(int $personId): void
{
if ($personId <= 0) {
return;
}
try {
Database::get()->prepare('DELETE FROM weread_snapshots WHERE person_id = ?')
->execute([$personId]);
} catch (\Throwable $e) {
}
}
}